From f65666ac4936110c77d178eca68a2de558b03954 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 6 Nov 2024 15:47:24 -0800 Subject: [PATCH 01/11] Update joiners.scad --- joiners.scad | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/joiners.scad b/joiners.scad index 45ff5bff..0090ac95 100644 --- a/joiners.scad +++ b/joiners.scad @@ -1223,6 +1223,8 @@ module rabbit_clip(type, length, width, snap, thickness, depth, compression=0.1 // Section: Splines // Module: hirth() +// Synopsis: Creates a Hirth face spline that locks together two cylinders. +// Syntags: Geom // Usage: // hirth(n, ir|id=, or|od=, tooth_angle, [cone_angle=], [chamfer=], [rounding=], [base=], [crop=], [anchor=], [spin=], [orient=] // Description: @@ -1232,7 +1234,7 @@ module rabbit_clip(type, length, width, snap, thickness, depth, compression=0.1 // Each tooth is a triangle that grows larger with radius. You specify a nominal tooth angle; the actual tooth // angle will be slightly different. // . -// You can also specify a cone_angle which raises or lowers the angle of the teeth. When you do this you ened to +// You can also specify a cone_angle which raises or lowers the angle of the teeth. When you do this you need to // mate splines with opposite angles such as -20 and +20. The splines appear centered at the origin so that two // splines will mate if their centers coincide. Therefore `attach(CENTER,CENTER)` will produce two mating splines // assuming that they are rotated correctly. The bottom anchors will be at the bottom of the spline base. The top From 0f82e6e9584f3f435632d6c05a18cb0fbdba9c87 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 5 Feb 2025 11:59:13 -0800 Subject: [PATCH 02/11] Update isosurface.scad --- isosurface.scad | 1484 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1484 insertions(+) diff --git a/isosurface.scad b/isosurface.scad index 78192971..84f6ae28 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // An isosurface is a three-dimensional surface representing points of a constant @@ -1411,3 +1412,1486 @@ for(i=[0:nballs-1]) let( 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 ]; +======= +///////////////////////////////////////////////////////////////////// +// LibFile: isosurface.scad +// [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), +// are bounded and closed organic-looking surfaces that smoothly meld together when in close proximity. +// Metaballs are a specific example of isosurfaces. +// . +// An isosurface is a three-dimensional surface representing points of a constant +// 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. Metaballs, for example, are typically generated by +// point sources (the metaball centers) that contribute values to all the points in a 3D volume depending +// on distance to the metaball centers. The combined contributions from all metaballs in the volume +// result variying values throughout the volume. Surfaces that connect all points of equal value in the +// volume that appear as blobby shapes that blend together. +// . +// In general temrs, an isosurface may be represented by any function of three variables, +// 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". +// We use the term "isovalue" in the context of metaballs also, although you may find other applications +// referring to "threshold", which means the same thing. +// . +// Some isosurface functions are unbounded, extending infinitely in all directions. A familiar example may +// be a [gryoid](https://en.wikipedia.org/wiki/Gyroid), which is often used as a volume infill pattern in +// [fused filament fabrication](https://en.wikipedia.org/wiki/Fused_filament_fabrication)). The gyroid +// isosurface is unbounded and periodic in all three dimensions. +// . +// Below are modules and functions to create manifold 3D models of metaballs and other isosurfaces. +// +// Includes: +// include +// include +// FileGroup: Advanced Modeling +// FileSummary: Isosurfaces and metaballs. +////////////////////////////////////////////////////////////////////// + + +/* +Lookup Tables for Transvoxel's Modified Marching Cubes + +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, a cube with corners 2, 3, and 7 greater than the threshold isovalue would have the index 10000110, an 8-bit binary number with bits 2, 3, and 7 set to 1, corresponding to decimal index 134. After determining the cube's index value this way, the triangulation order is looked up in a table. + +Axes are + z + (top) + | y (back) + | / + |/ + +----- x (right) + +Vertex and edge layout (heavier = and # indicate closer to viewer): + + 3 +----------+ 7 +----10----+ + /: /| /: /| + / : / | 1 2 5 6 + 1 +==========+5 | +=====9====+ | + # 2+ - - - # -+ 6 # +- - 11-# -+ + # / # / 0 3 4 7 + #/ #/ #/ #/ + 0 +==========+ 4 +=====8=====+ + +z changes fastest, then y, then x. + +----------------------------------------------------------- +Addition by Alex: +Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. + + 4(back) + 3 +----------+ 7 + /: 5(top) /| + / : / | + 1 +==========+5 | <-- 3 (side) +0(side) --> # 2+ - - - # -+ 6 + # / # / + #/ 2(bot) #/ + 0 +----------+ 4 + 1(front) +*/ + +/// four indices for each face of the cube, counterclockwise looking from inside out +_MCFaceVertexIndices = [ + [], + [0,2,3,1], // left, x=0 plane + [0,1,5,4], // front, y=0 plane + [0,4,6,2], // bottom, z=0 plane + [4,5,7,6], // right, x=voxsize plane + [2,6,7,3], // back, y=voxsize plane + [1,3,7,5], // top, z=voxsize plane +]; + +/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. +function _bbox_faces(v0, voxsize, bbox) = let( + a = v0-bbox[0], + bb1 = bbox[1] - [voxsize,voxsize,voxsize], + b = v0-bb1 +) [ + if(a[0]==0) 1, + if(a[1]==0) 2, + if(a[2]==0) 3, + if(b[0]>=0) 4, + if(b[1]>=0) 5, + if(b[2]>=0) 6 +]; +/// End of bounding-box face-clipping stuff. Back to the marching cubes triangulation.... + + +/// Pair of vertex indices for each edge on the voxel +_MCEdgeVertexIndices = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [3, 7], + [2, 6] +]; + +/// 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], + [1,0,9], + [9,1,8,8,1,3], + [3,2,11], + [2,11,0,0,11,8], + [1,0,9,3,2,11], + [11,1,2,11,9,1,11,8,9], + [10,2,1], + [2,1,10,0,3,8], + [0,9,2,2,9,10], + [8,2,3,8,10,2,8,9,10], + [1,10,3,3,10,11], + [10,0,1,10,8,0,10,11,8], + [9,3,0,9,11,3,9,10,11], + [9,10,8,8,10,11], + [7,4,8], + [0,3,4,4,3,7], + [0,9,1,4,8,7], + [1,4,9,1,7,4,1,3,7], + [11,3,2,8,7,4], + [4,11,7,4,2,11,4,0,2], + [3,2,11,0,9,1,4,8,7], + [9,1,4,4,1,7,7,1,2,7,2,11], + [7,4,8,1,10,2], + [7,4,3,3,4,0,10,2,1], + [10,2,9,9,2,0,7,4,8], + [7,4,9,7,9,2,9,10,2,3,7,2], + [1,10,3,3,10,11,4,8,7], + [4,0,7,0,1,10,7,0,10,7,10,11], + [7,4,8,9,3,0,9,11,3,9,10,11], + [7,4,11,4,9,11,9,10,11], + [5,9,4], + [8,0,3,9,4,5], + [1,0,5,5,0,4], + [5,8,4,5,3,8,5,1,3], + [3,2,11,5,9,4], + [2,11,0,0,11,8,5,9,4], + [4,5,0,0,5,1,11,3,2], + [11,8,2,8,4,5,2,8,5,2,5,1], + [5,9,4,1,10,2], + [0,3,8,1,10,2,5,9,4], + [2,5,10,2,4,5,2,0,4], + [4,5,8,8,5,3,3,5,10,3,10,2], + [11,3,10,10,3,1,4,5,9], + [4,5,9,10,0,1,10,8,0,10,11,8], + [4,5,10,4,10,3,10,11,3,0,4,3], + [4,5,8,5,10,8,10,11,8], + [5,9,7,7,9,8], + [3,9,0,3,5,9,3,7,5], + [7,0,8,7,1,0,7,5,1], + [3,7,1,1,7,5], + [5,9,7,7,9,8,2,11,3], + [5,9,0,5,0,11,0,2,11,7,5,11], + [2,11,3,7,0,8,7,1,0,7,5,1], + [2,11,1,11,7,1,7,5,1], + [8,7,9,9,7,5,2,1,10], + [10,2,1,3,9,0,3,5,9,3,7,5], + [2,0,10,0,8,7,10,0,7,10,7,5], + [10,2,5,2,3,5,3,7,5], + [5,9,8,5,8,7,1,10,3,10,11,3], + [1,10,0,0,10,11,0,11,7,0,7,5,0,5,9], + [8,7,0,0,7,5,0,5,10,0,10,11,0,11,3], + [5,11,7,10,11,5], + [11,6,7], + [3,8,0,7,11,6], + [1,0,9,7,11,6], + [9,1,8,8,1,3,6,7,11], + [6,7,2,2,7,3], + [0,7,8,0,6,7,0,2,6], + [6,7,2,2,7,3,9,1,0], + [9,1,2,9,2,7,2,6,7,8,9,7], + [10,2,1,11,6,7], + [2,1,10,3,8,0,7,11,6], + [0,9,2,2,9,10,7,11,6], + [6,7,11,8,2,3,8,10,2,8,9,10], + [7,10,6,7,1,10,7,3,1], + [1,10,0,0,10,8,8,10,6,8,6,7], + [9,10,0,10,6,7,0,10,7,0,7,3], + [6,7,10,7,8,10,8,9,10], + [4,8,6,6,8,11], + [6,3,11,6,0,3,6,4,0], + [11,6,8,8,6,4,1,0,9], + [6,4,11,4,9,1,11,4,1,11,1,3], + [2,8,3,2,4,8,2,6,4], + [0,2,4,4,2,6], + [9,1,0,2,8,3,2,4,8,2,6,4], + [9,1,4,1,2,4,2,6,4], + [4,8,6,6,8,11,1,10,2], + [1,10,2,6,3,11,6,0,3,6,4,0], + [0,9,10,0,10,2,4,8,6,8,11,6], + [11,6,3,3,6,4,3,4,9,3,9,10,3,10,2], + [1,10,6,1,6,8,6,4,8,3,1,8], + [1,10,0,10,6,0,6,4,0], + [0,9,3,3,9,10,3,10,6,3,6,4,3,4,8], + [4,10,6,9,10,4], + [4,5,9,6,7,11], + [7,11,6,8,0,3,9,4,5], + [1,0,5,5,0,4,11,6,7], + [11,6,7,5,8,4,5,3,8,5,1,3], + [3,2,7,7,2,6,9,4,5], + [5,9,4,0,7,8,0,6,7,0,2,6], + [1,0,4,1,4,5,3,2,7,2,6,7], + [4,5,8,8,5,1,8,1,2,8,2,6,8,6,7], + [6,7,11,5,9,4,1,10,2], + [5,9,4,7,11,6,0,3,8,2,1,10], + [7,11,6,2,5,10,2,4,5,2,0,4], + [6,7,11,3,8,4,3,4,5,3,5,2,2,5,10], + [9,4,5,7,10,6,7,1,10,7,3,1], + [5,9,4,8,0,1,8,1,10,8,10,7,7,10,6], + [6,7,10,10,7,3,10,3,0,10,0,4,10,4,5], + [4,5,8,8,5,10,8,10,6,8,6,7], + [9,6,5,9,11,6,9,8,11], + [0,3,9,9,3,5,5,3,11,5,11,6], + [1,0,8,1,8,6,8,11,6,5,1,6], + [11,6,3,6,5,3,5,1,3], + [2,6,3,6,5,9,3,6,9,3,9,8], + [5,9,6,9,0,6,0,2,6], + [3,2,8,8,2,6,8,6,5,8,5,1,8,1,0], + [1,6,5,2,6,1], + [2,1,10,9,6,5,9,11,6,9,8,11], + [2,1,10,5,9,0,5,0,3,5,3,6,6,3,11], + [10,2,5,5,2,0,5,0,8,5,8,11,5,11,6], + [10,2,5,5,2,3,5,3,11,5,11,6], + [5,9,6,6,9,8,6,8,3,6,3,1,6,1,10], + [5,9,6,6,9,0,6,0,1,6,1,10], + [8,3,0,5,10,6], + [6,5,10], + [6,10,5], + [3,8,0,5,6,10], + [9,1,0,10,5,6], + [3,8,1,1,8,9,6,10,5], + [6,10,5,2,11,3], + [8,0,11,11,0,2,5,6,10], + [10,5,6,1,0,9,3,2,11], + [5,6,10,11,1,2,11,9,1,11,8,9], + [2,1,6,6,1,5], + [5,6,1,1,6,2,8,0,3], + [6,9,5,6,0,9,6,2,0], + [8,9,3,9,5,6,3,9,6,3,6,2], + [3,6,11,3,5,6,3,1,5], + [5,6,11,5,11,0,11,8,0,1,5,0], + [0,9,3,3,9,11,11,9,5,11,5,6], + [5,6,9,6,11,9,11,8,9], + [7,4,8,5,6,10], + [0,3,4,4,3,7,10,5,6], + [4,8,7,9,1,0,10,5,6], + [6,10,5,1,4,9,1,7,4,1,3,7], + [11,3,2,7,4,8,5,6,10], + [10,5,6,4,11,7,4,2,11,4,0,2], + [7,4,8,3,2,11,9,1,0,10,5,6], + [10,5,6,7,4,9,7,9,1,7,1,11,11,1,2], + [2,1,6,6,1,5,8,7,4], + [7,4,0,7,0,3,5,6,1,6,2,1], + [8,7,4,6,9,5,6,0,9,6,2,0], + [5,6,9,9,6,2,9,2,3,9,3,7,9,7,4], + [4,8,7,3,6,11,3,5,6,3,1,5], + [7,4,11,11,4,0,11,0,1,11,1,5,11,5,6], + [4,8,7,11,3,0,11,0,9,11,9,6,6,9,5], + [5,6,9,9,6,11,9,11,7,9,7,4], + [9,4,10,10,4,6], + [6,10,4,4,10,9,3,8,0], + [0,10,1,0,6,10,0,4,6], + [3,8,4,3,4,10,4,6,10,1,3,10], + [9,4,10,10,4,6,3,2,11], + [8,0,2,8,2,11,9,4,10,4,6,10], + [11,3,2,0,10,1,0,6,10,0,4,6], + [2,11,1,1,11,8,1,8,4,1,4,6,1,6,10], + [4,1,9,4,2,1,4,6,2], + [3,8,0,4,1,9,4,2,1,4,6,2], + [4,6,0,0,6,2], + [3,8,2,8,4,2,4,6,2], + [3,1,11,1,9,4,11,1,4,11,4,6], + [9,4,1,1,4,6,1,6,11,1,11,8,1,8,0], + [11,3,6,3,0,6,0,4,6], + [8,6,11,4,6,8], + [10,7,6,10,8,7,10,9,8], + [10,9,6,9,0,3,6,9,3,6,3,7], + [8,7,0,0,7,1,1,7,6,1,6,10], + [6,10,7,10,1,7,1,3,7], + [3,2,11,10,7,6,10,8,7,10,9,8], + [6,10,7,7,10,9,7,9,0,7,0,2,7,2,11], + [11,3,2,1,0,8,1,8,7,1,7,10,10,7,6], + [6,10,7,7,10,1,7,1,2,7,2,11], + [8,7,6,8,6,1,6,2,1,9,8,1], + [0,3,9,9,3,7,9,7,6,9,6,2,9,2,1], + [8,7,0,7,6,0,6,2,0], + [7,2,3,6,2,7], + [11,3,6,6,3,1,6,1,9,6,9,8,6,8,7], + [11,7,6,1,9,0], + [11,3,6,6,3,0,6,0,8,6,8,7], + [11,7,6], + [10,5,11,11,5,7], + [10,5,11,11,5,7,0,3,8], + [7,11,5,5,11,10,0,9,1], + [3,8,9,3,9,1,7,11,5,11,10,5], + [5,2,10,5,3,2,5,7,3], + [0,2,8,2,10,5,8,2,5,8,5,7], + [0,9,1,5,2,10,5,3,2,5,7,3], + [10,5,2,2,5,7,2,7,8,2,8,9,2,9,1], + [1,11,2,1,7,11,1,5,7], + [8,0,3,1,11,2,1,7,11,1,5,7], + [0,9,5,0,5,11,5,7,11,2,0,11], + [3,8,2,2,8,9,2,9,5,2,5,7,2,7,11], + [5,7,1,1,7,3], + [8,0,7,0,1,7,1,5,7], + [0,9,3,9,5,3,5,7,3], + [9,7,8,5,7,9], + [8,5,4,8,10,5,8,11,10], + [10,5,4,10,4,3,4,0,3,11,10,3], + [1,0,9,8,5,4,8,10,5,8,11,10], + [9,1,4,4,1,3,4,3,11,4,11,10,4,10,5], + [10,5,2,2,5,3,3,5,4,3,4,8], + [10,5,2,5,4,2,4,0,2], + [9,1,0,3,2,10,3,10,5,3,5,8,8,5,4], + [10,5,2,2,5,4,2,4,9,2,9,1], + [1,5,2,5,4,8,2,5,8,2,8,11], + [2,1,11,11,1,5,11,5,4,11,4,0,11,0,3], + [4,8,5,5,8,11,5,11,2,5,2,0,5,0,9], + [5,4,9,2,3,11], + [4,8,5,8,3,5,3,1,5], + [0,5,4,1,5,0], + [0,9,3,3,9,5,3,5,4,3,4,8], + [5,4,9], + [11,4,7,11,9,4,11,10,9], + [0,3,8,11,4,7,11,9,4,11,10,9], + [0,4,1,4,7,11,1,4,11,1,11,10], + [7,11,4,4,11,10,4,10,1,4,1,3,4,3,8], + [9,4,7,9,7,2,7,3,2,10,9,2], + [8,0,7,7,0,2,7,2,10,7,10,9,7,9,4], + [1,0,10,10,0,4,10,4,7,10,7,3,10,3,2], + [7,8,4,10,1,2], + [9,4,1,1,4,2,2,4,7,2,7,11], + [8,0,3,2,1,9,2,9,4,2,4,11,11,4,7], + [7,11,4,11,2,4,2,0,4], + [3,8,2,2,8,4,2,4,7,2,7,11], + [9,4,1,4,7,1,7,3,1], + [9,4,1,1,4,7,1,7,8,1,8,0], + [3,4,7,0,4,3], + [7,8,4], + [8,11,9,9,11,10], + [0,3,9,3,11,9,11,10,9], + [1,0,10,0,8,10,8,11,10], + [10,3,11,1,3,10], + [3,2,8,2,10,8,10,9,8], + [9,2,10,0,2,9], + [1,0,10,10,0,8,10,8,3,10,3,2], + [2,10,1], + [2,1,11,1,9,11,9,8,11], + [2,1,11,11,1,9,11,9,0,11,0,3], + [11,0,8,2,0,11], + [3,11,2], + [1,8,3,9,8,1], + [1,9,0], + [8,3,0], + [] +]; + +/// Same list as above, but with each row in reverse order. Needed for generating shells (two isosurfaces at slightly different iso values). +/// More efficient just to have a static table than to generate it each time by calling reverse() hundreds of times (although this static table was generated that way). +_MCTriangleTable_reverse = [ + [], + [0,8,3], + [9,0,1], + [3,1,8,8,1,9], + [11,2,3], + [8,11,0,0,11,2], + [11,2,3,9,0,1], + [9,8,11,1,9,11,2,1,11], + [1,2,10], + [8,3,0,10,1,2], + [10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8], + [11,10,3,3,10,1], + [8,11,10,0,8,10,1,0,10], + [11,10,9,3,11,9,0,3,9], + [11,10,8,8,10,9], + [8,4,7], + [7,3,4,4,3,0], + [7,8,4,1,9,0], + [7,3,1,4,7,1,9,4,1], + [4,7,8,2,3,11], + [2,0,4,11,2,4,7,11,4], + [7,8,4,1,9,0,11,2,3], + [11,2,7,2,1,7,7,1,4,4,1,9], + [2,10,1,8,4,7], + [1,2,10,0,4,3,3,4,7], + [8,4,7,0,2,9,9,2,10], + [2,7,3,2,10,9,2,9,7,9,4,7], + [7,8,4,11,10,3,3,10,1], + [11,10,7,10,0,7,10,1,0,7,0,4], + [11,10,9,3,11,9,0,3,9,8,4,7], + [11,10,9,11,9,4,11,4,7], + [4,9,5], + [5,4,9,3,0,8], + [4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5], + [4,9,5,11,2,3], + [4,9,5,8,11,0,0,11,2], + [2,3,11,1,5,0,0,5,4], + [1,5,2,5,8,2,5,4,8,2,8,11], + [2,10,1,4,9,5], + [4,9,5,2,10,1,8,3,0], + [4,0,2,5,4,2,10,5,2], + [2,10,3,10,5,3,3,5,8,8,5,4], + [9,5,4,1,3,10,10,3,11], + [8,11,10,0,8,10,1,0,10,9,5,4], + [3,4,0,3,11,10,3,10,4,10,5,4], + [8,11,10,8,10,5,8,5,4], + [8,9,7,7,9,5], + [5,7,3,9,5,3,0,9,3], + [1,5,7,0,1,7,8,0,7], + [5,7,1,1,7,3], + [3,11,2,8,9,7,7,9,5], + [11,5,7,11,2,0,11,0,5,0,9,5], + [1,5,7,0,1,7,8,0,7,3,11,2], + [1,5,7,1,7,11,1,11,2], + [10,1,2,5,7,9,9,7,8], + [5,7,3,9,5,3,0,9,3,1,2,10], + [5,7,10,7,0,10,7,8,0,10,0,2], + [5,7,3,5,3,2,5,2,10], + [3,11,10,3,10,1,7,8,5,8,9,5], + [9,5,0,5,7,0,7,11,0,11,10,0,0,10,1], + [3,11,0,11,10,0,10,5,0,5,7,0,0,7,8], + [5,11,10,7,11,5], + [7,6,11], + [6,11,7,0,8,3], + [6,11,7,9,0,1], + [11,7,6,3,1,8,8,1,9], + [3,7,2,2,7,6], + [6,2,0,7,6,0,8,7,0], + [0,1,9,3,7,2,2,7,6], + [7,9,8,7,6,2,7,2,9,2,1,9], + [7,6,11,1,2,10], + [6,11,7,0,8,3,10,1,2], + [6,11,7,10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8,11,7,6], + [1,3,7,10,1,7,6,10,7], + [7,6,8,6,10,8,8,10,0,0,10,1], + [3,7,0,7,10,0,7,6,10,0,10,9], + [10,9,8,10,8,7,10,7,6], + [11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6], + [9,0,1,4,6,8,8,6,11], + [3,1,11,1,4,11,1,9,4,11,4,6], + [4,6,2,8,4,2,3,8,2], + [6,2,4,4,2,0], + [4,6,2,8,4,2,3,8,2,0,1,9], + [4,6,2,4,2,1,4,1,9], + [2,10,1,11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6,2,10,1], + [6,11,8,6,8,4,2,10,0,10,9,0], + [2,10,3,10,9,3,9,4,3,4,6,3,3,6,11], + [8,1,3,8,4,6,8,6,1,6,10,1], + [0,4,6,0,6,10,0,10,1], + [8,4,3,4,6,3,6,10,3,10,9,3,3,9,0], + [4,10,9,6,10,4], + [11,7,6,9,5,4], + [5,4,9,3,0,8,6,11,7], + [7,6,11,4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5,7,6,11], + [5,4,9,6,2,7,7,2,3], + [6,2,0,7,6,0,8,7,0,4,9,5], + [7,6,2,7,2,3,5,4,1,4,0,1], + [7,6,8,6,2,8,2,1,8,1,5,8,8,5,4], + [2,10,1,4,9,5,11,7,6], + [10,1,2,8,3,0,6,11,7,4,9,5], + [4,0,2,5,4,2,10,5,2,6,11,7], + [10,5,2,2,5,3,5,4,3,4,8,3,11,7,6], + [1,3,7,10,1,7,6,10,7,5,4,9], + [6,10,7,7,10,8,10,1,8,1,0,8,4,9,5], + [5,4,10,4,0,10,0,3,10,3,7,10,10,7,6], + [7,6,8,6,10,8,10,5,8,8,5,4], + [11,8,9,6,11,9,5,6,9], + [6,11,5,11,3,5,5,3,9,9,3,0], + [6,1,5,6,11,8,6,8,1,8,0,1], + [3,1,5,3,5,6,3,6,11], + [8,9,3,9,6,3,9,5,6,3,6,2], + [6,2,0,6,0,9,6,9,5], + [0,1,8,1,5,8,5,6,8,6,2,8,8,2,3], + [1,6,2,5,6,1], + [11,8,9,6,11,9,5,6,9,10,1,2], + [11,3,6,6,3,5,3,0,5,0,9,5,10,1,2], + [6,11,5,11,8,5,8,0,5,0,2,5,5,2,10], + [6,11,5,11,3,5,3,2,5,5,2,10], + [10,1,6,1,3,6,3,8,6,8,9,6,6,9,5], + [10,1,6,1,0,6,0,9,6,6,9,5], + [6,10,5,0,3,8], + [10,5,6], + [5,10,6], + [10,6,5,0,8,3], + [6,5,10,0,1,9], + [5,10,6,9,8,1,1,8,3], + [3,11,2,5,10,6], + [10,6,5,2,0,11,11,0,8], + [11,2,3,9,0,1,6,5,10], + [9,8,11,1,9,11,2,1,11,10,6,5], + [5,1,6,6,1,2], + [3,0,8,2,6,1,1,6,5], + [0,2,6,9,0,6,5,9,6], + [2,6,3,6,9,3,6,5,9,3,9,8], + [5,1,3,6,5,3,11,6,3], + [0,5,1,0,8,11,0,11,5,11,6,5], + [6,5,11,5,9,11,11,9,3,3,9,0], + [9,8,11,9,11,6,9,6,5], + [10,6,5,8,4,7], + [6,5,10,7,3,4,4,3,0], + [6,5,10,0,1,9,7,8,4], + [7,3,1,4,7,1,9,4,1,5,10,6], + [10,6,5,8,4,7,2,3,11], + [2,0,4,11,2,4,7,11,4,6,5,10], + [6,5,10,0,1,9,11,2,3,8,4,7], + [2,1,11,11,1,7,1,9,7,9,4,7,6,5,10], + [4,7,8,5,1,6,6,1,2], + [1,2,6,1,6,5,3,0,7,0,4,7], + [0,2,6,9,0,6,5,9,6,4,7,8], + [4,7,9,7,3,9,3,2,9,2,6,9,9,6,5], + [5,1,3,6,5,3,11,6,3,7,8,4], + [6,5,11,5,1,11,1,0,11,0,4,11,11,4,7], + [5,9,6,6,9,11,9,0,11,0,3,11,7,8,4], + [4,7,9,7,11,9,11,6,9,9,6,5], + [6,4,10,10,4,9], + [0,8,3,9,10,4,4,10,6], + [6,4,0,10,6,0,1,10,0], + [10,3,1,10,6,4,10,4,3,4,8,3], + [11,2,3,6,4,10,10,4,9], + [10,6,4,10,4,9,11,2,8,2,0,8], + [6,4,0,10,6,0,1,10,0,2,3,11], + [10,6,1,6,4,1,4,8,1,8,11,1,1,11,2], + [2,6,4,1,2,4,9,1,4], + [2,6,4,1,2,4,9,1,4,0,8,3], + [2,6,0,0,6,4], + [2,6,4,2,4,8,2,8,3], + [6,4,11,4,1,11,4,9,1,11,1,3], + [0,8,1,8,11,1,11,6,1,6,4,1,1,4,9], + [6,4,0,6,0,3,6,3,11], + [8,6,4,11,6,8], + [8,9,10,7,8,10,6,7,10], + [7,3,6,3,9,6,3,0,9,6,9,10], + [10,6,1,6,7,1,1,7,0,0,7,8], + [7,3,1,7,1,10,7,10,6], + [8,9,10,7,8,10,6,7,10,11,2,3], + [11,2,7,2,0,7,0,9,7,9,10,7,7,10,6], + [6,7,10,10,7,1,7,8,1,8,0,1,2,3,11], + [11,2,7,2,1,7,1,10,7,7,10,6], + [1,8,9,1,2,6,1,6,8,6,7,8], + [1,2,9,2,6,9,6,7,9,7,3,9,9,3,0], + [0,2,6,0,6,7,0,7,8], + [7,2,6,3,2,7], + [7,8,6,8,9,6,9,1,6,1,3,6,6,3,11], + [0,9,1,6,7,11], + [7,8,6,8,0,6,0,3,6,6,3,11], + [6,7,11], + [7,5,11,11,5,10], + [8,3,0,7,5,11,11,5,10], + [1,9,0,10,11,5,5,11,7], + [5,10,11,5,11,7,1,9,3,9,8,3], + [3,7,5,2,3,5,10,2,5], + [7,5,8,5,2,8,5,10,2,8,2,0], + [3,7,5,2,3,5,10,2,5,1,9,0], + [1,9,2,9,8,2,8,7,2,7,5,2,2,5,10], + [7,5,1,11,7,1,2,11,1], + [7,5,1,11,7,1,2,11,1,3,0,8], + [11,0,2,11,7,5,11,5,0,5,9,0], + [11,7,2,7,5,2,5,9,2,9,8,2,2,8,3], + [3,7,1,1,7,5], + [7,5,1,7,1,0,7,0,8], + [3,7,5,3,5,9,3,9,0], + [9,7,5,8,7,9], + [10,11,8,5,10,8,4,5,8], + [3,10,11,3,0,4,3,4,10,4,5,10], + [10,11,8,5,10,8,4,5,8,9,0,1], + [5,10,4,10,11,4,11,3,4,3,1,4,4,1,9], + [8,4,3,4,5,3,3,5,2,2,5,10], + [2,0,4,2,4,5,2,5,10], + [4,5,8,8,5,3,5,10,3,10,2,3,0,1,9], + [1,9,2,9,4,2,4,5,2,2,5,10], + [11,8,2,8,5,2,8,4,5,2,5,1], + [3,0,11,0,4,11,4,5,11,5,1,11,11,1,2], + [9,0,5,0,2,5,2,11,5,11,8,5,5,8,4], + [11,3,2,9,4,5], + [5,1,3,5,3,8,5,8,4], + [0,5,1,4,5,0], + [8,4,3,4,5,3,5,9,3,3,9,0], + [9,4,5], + [9,10,11,4,9,11,7,4,11], + [9,10,11,4,9,11,7,4,11,8,3,0], + [10,11,1,11,4,1,11,7,4,1,4,0], + [8,3,4,3,1,4,1,10,4,10,11,4,4,11,7], + [2,9,10,2,3,7,2,7,9,7,4,9], + [4,9,7,9,10,7,10,2,7,2,0,7,7,0,8], + [2,3,10,3,7,10,7,4,10,4,0,10,10,0,1], + [2,1,10,4,8,7], + [11,7,2,7,4,2,2,4,1,1,4,9], + [7,4,11,11,4,2,4,9,2,9,1,2,3,0,8], + [4,0,2,4,2,11,4,11,7], + [11,7,2,7,4,2,4,8,2,2,8,3], + [1,3,7,1,7,4,1,4,9], + [0,8,1,8,7,1,7,4,1,1,4,9], + [3,4,0,7,4,3], + [4,8,7], + [10,11,9,9,11,8], + [9,10,11,9,11,3,9,3,0], + [10,11,8,10,8,0,10,0,1], + [10,3,1,11,3,10], + [8,9,10,8,10,2,8,2,3], + [9,2,0,10,2,9], + [2,3,10,3,8,10,8,0,10,10,0,1], + [1,10,2], + [11,8,9,11,9,1,11,1,2], + [3,0,11,0,9,11,9,1,11,11,1,2], + [11,0,2,8,0,11], + [2,11,3], + [1,8,9,3,8,1], + [0,9,1], + [0,3,8], + [] +]; + + +/// _cubindex() - private function, called by _isosurface_cubes() +/// Return the index ID of a voxel depending on the field strength at each corner exceeding isoval. +function _cubeindex(f, isoval) = + (f[0] > isoval ? 1 : 0) + + (f[1] > isoval ? 2 : 0) + + (f[2] > isoval ? 4 : 0) + + (f[3] > isoval ? 8 : 0) + + (f[4] > isoval ? 16 : 0) + + (f[5] > isoval ? 32 : 0) + + (f[6] > isoval ? 64 : 0) + + (f[7] > isoval ? 128 : 0); + + +/// isosurface_cubes() - private function, called by isosurface() +/// This implements a marching cube algorithm, sacrificing some memory in favor of speed. +/// Return a list of voxel cube structures that have one or both surfaces isovalmin or isovalmax intersecting them, and cubes inside the isosurface volume that are at the bounds of the bounding box. +/// The cube structure is: +/// [cubecoord, cubeindex_isomin, cubeindex_isomax, field, bfaces] +/// where +/// cubecoord is the [x,y,z] coordinate of the front left bottom corner of the voxel, +/// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections +/// cf is vector containing the 6 field strength values at each corner of the voxel cube +/// 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, isovalmin, isovalmax, closed=true) = let( + // get field intensities + 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]]) + fieldfunc(x,y,z) + ] + ] + ], + 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) + for(j=[0:ny]) let(y=v0[1]+voxsize*j) + for(k=[0:nz]) let(z=v0[2]+voxsize*k) + let(i1=i+1, j1=j+1, k1=k+1, + cf = [ // cube corner field values + 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 = closed ? _bbox_faces(cubecoord, voxsize, bbox) : [], + cubefound_isomin = (mincf<=isovalmin && isovalmin0 && lenmax>0) let( + + // both min and max surfaces intersect a voxel clipped by bounding box + list = concat( + // min surface + [ for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], + // max surface + [ for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(n_outer>0 && lenmin>0) let( + + // only min surface intersects a voxel clipped by bounding box + list = concat( + [ for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(lenmin>0) + + // only min surface intersects a voxel + for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + else if(n_outer>0 && lenmax>0) let( + + // only max surface intersects the voxel on the bounding box + list = concat( + [ for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(lenmax>0) + + // only max surface intersects the voxel + for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + else if(n_outer>0) + + // no surface intersects a voxel clipped by bounding box but the bounding box at this voxel is inside the volume between isomin and isomax + for(ls = outfacevertices) ls +]; + + +/// Generate triangles for the special case of voxel faces clipped by the bounding box +function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( + vi = _MCFaceVertexIndices[bbface], + vfc = [ for(i=vi) vcube[i] ], + fld = [ for(i=vi) f[i] ], + pgon = flatten([ + for(i=[0:3]) let( + vi0=vi[i], + vi1=vi[(i+1)%4], + f0 = f[vi0], + f1 = f[vi1], + lowhiorder = (f0=f1) let( + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f0)/denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(fbetweenlow && f0>=f1) let( + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f0)/denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + + ] + ]), + npgon = len(pgon), + triangles = npgon==0 ? [] : [ + for(i=[1:len(pgon)-2]) [pgon[0], pgon[i], pgon[i+1]] + ]) flatten(triangles); + + +/// _showstats() (Private function) - called by isosurface() and isosurface_array() +/// Display statistics about isosurface +function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( + v = column(cubes, 0), // extract cube vertices + x = column(v,0), // extract x values + y = column(v,1), // extract y values + z = column(v,2), // extract z values + xmin = min(x), + xmax = max(x)+voxelsize, + ymin = min(y), + ymax = max(y)+voxelsize, + zmin = min(z), + zmax = max(z)+voxelsize, + ntri = len(faces), + nvox = len(cubes) +) echo(str("\nIsosurface statistics:\n Outer isovalue = ", isoval, "\n Voxel size = ", voxelsize, + "\n Voxels found containing surface = ", nvox, "\n Triangles = ", ntri, + "\n Voxel bounding box for all data = ", bbox, + "\n Voxel bounding box for isosurface = ", [[xmin,ymin,zmin], [xmax,ymax,zmax]], + "\n")) 0; + + +/// ---------- metaball stuff starts here ---------- + +/// Animated metaball demo made with BOSL2 here: https://imgur.com/a/m29q8Qd + +/// Built-in metaball functions corresponding to each MB_ index. +/// Each function takes three parameters: +/// dv = cartesian distance, a vector [dx,dy,dz] being the distances from the ball center to the volume sample point +/// coeff = the intensity (weight, charge, density, etc.) of the metaball, can be a vector if warranted. +/// additional value or array of values needed by the function. +/// cutoff = radial cutoff distance; effect suppression increases with distance until zero at the cutoff distance, and is zero from that point farther out. Default: INF +/// influence = inverse exponent to 1/r, the higher the influence, the further the "reach" of the metaball. Default: 1 + +/// metaball field function, calling any of the other metaball functions above to accumulate +/// the contribution of each metaball at point xyz + +/* +/// metaball suppression and cutoff function to control shape of the falloff +function mb_shaping(dist, coeff, cutoff, influence) = + sign(influence) * 0.5*(cos(180*(dist/cutoff)^4)+1) + * (coeff/dist)^(1/abs(influence)); + + +function _mb_sphere(dv, coeff, cutoff, influence) = + let(r=norm(dv)) r>=cutoff ? 0 + : mb_shaping(r, coeff, cutoff, influence); + + +//function mb_sphere(coeff=10, cutoff=INF, influence=1) = function (dv) _mb_sphere(dv, coeff, cutoff, influence); +*/ + + +/// metaball cutoff function + +function mb_cutoff(dist, cutoff) = dist>=cutoff ? 0 : 0.5*(cos(180*(dist/cutoff)^4)+1); + +/// metaball sphere + +function _mb_sphere_basic(dv, r, neg) = neg*r/norm(dv); +function _mb_sphere_influence(dv, r, ex, neg) = neg * (r/norm(dv))^ex; +function _mb_sphere_cutoff(dv, r, cutoff, neg) = let(dist=norm(dv)) + neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_sphere_full(dv, r, ex, cutoff, neg) = let(dist=norm(dv)) + neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_sphere_basic(dv,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_sphere_influence(dv,r,1/influence, neg) + : influence==1 ? function(dv) _mb_sphere_cutoff(dv,r,cutoff,neg) + : function(dv) _mb_sphere_full(dv,r,1/influence,cutoff,neg); + +// metaball cylinder + +function _mb_cylinder_basic(dv, r, hl, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z0, "\ncutoff must be a positive number.") + assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_num(length) && length>0, "\nlength must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_cylinder_basic(dv,r,length/2,neg) + : !is_finite(cutoff) ? function(dv) _mb_cylinder_influence(dv,r,length/2,1/influence, neg) + : influence==1 ? function(dv)_mb_cylinder_cutoff(dv,r,length/2,cutoff,neg) + : function (dv) _mb_cylinder_full(dv, r, length/2, 1/influence, cutoff, neg); + +// metaball rounded cube + +function _mb_roundcube_basic(dv, siz, xp, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg*siz/dist; +function _mb_roundcube_influence(dv, siz, xp, ex, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * (siz/dist)^ex; +function _mb_roundcube_cutoff(dv, siz, xp, cutoff, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) * siz/dist; +function _mb_roundcube_full(dv, siz, xp, ex, cutoff, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) * (siz/dist)^ex; + +function mb_roundcube(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_num(size) && size>0, "\nsize must be a positive number.") + let( + dummy=assert(is_num(size) && size>0, "\ninvalid size."), + xp = _squircle_se_exponent(squareness), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_roundcube_basic(dv, size/2, xp, neg) +: !is_finite(cutoff) ? function(dv) _mb_roundcube_influence(dv, size/2, xp, 1/influence, neg) +: influence==1 ? function(dv) _mb_roundcube_cutoff(dv, size/2, xp, cutoff, neg) +: function (dv) _mb_roundcube_full(dv, size/2, xp, 1/influence, cutoff, neg); + +// metaball octahedron + +function _mb_octahedron_basic(dv, r, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg*r/dist; +function _mb_octahedron_influence(dv, r, ex, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * (r/dist)^ex; +function _mb_octahedron_cutoff(dv, r, cutoff, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_octahedron_full(dv, r, ex, cutoff, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_octahedron_basic(dv,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_octahedron_influence(dv,r,1/influence, neg) + : influence==1 ? function(dv) _mb_octahedron_cutoff(dv,r,cutoff,neg) + : function(dv) _mb_octahedron_full(dv,r,1/influence,cutoff,neg); + +// torus + +function _mb_torus_basic(dv, rmaj, rmin, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg*rmin/dist; +function _mb_torus_influence(dv, rmaj, rmin, ex, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * (rmin/dist)^ex; +function _mb_torus_cutoff(dv, rmaj, rmin, cutoff, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) + neg * mb_cutoff(dist, cutoff) * rmin/dist; +function _mb_torus_full(dv, rmaj, rmin, ex, cutoff, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) + neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; + +function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r_maj = get_radius(r=r_maj,d=d_maj), + r_min = get_radius(r=r_min,d=d_min), + dum1=assert(is_finite(r_maj) && r_maj>0, "\ninvalid major radius or diameter."), + dum2=assert(is_finite(r_min) && r_min>0, "\ninvalid minor radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_torus_basic(dv, r_maj, r_min, neg) + : !is_finite(cutoff) ? function(dv) _mb_torus_influence(dv, r_maj, r_min, 1/influence, neg) + : influence==1 ? function(dv) _mb_torus_cutoff(dv, r_maj, r_min, cutoff, neg) + : function(dv) _mb_torus_full(dv, r_maj, r_min, 1/influence, cutoff, neg); + + + +/* hard-edge cylinder +_mb_cylinder = function (dv, coeff, length, cutoff, influence) +let( + dist = max(abs(dv.z*2*coeff/length), norm([dv.x,dv.y])), + suppress = let(a = min(r,cutoff)/cutoff) 1-a*a, +) suppress*coeff / dist; + +function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (dv) _mb_cylinder(dv, coeff, length, cutoff, influence); +*/ + + +// Function&Module: metaballs() +// Synopsis: Creates a model of metaballs within a bounding box. +// SynTags: Geom,VNF +// Topics: Metaballs, Isosurfaces, VNF Generators +// See Also: isosurface_array() +// Usage: As a module +// metaballs(funcs, bounding_box, voxel_size, [isovalue=], [closed=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = metaballs(funcs, bounding_box, voxel_size, [isovalue=], [closed=], [show_stats=]); +// Description: +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", +// are organic-looking ball-shaped blobs that meld together when in close proximity. +// The melding property is determined by an interaction formula based on the coefficient +// weight (which can be thought of as a charge, strength, density, or intensity) of +// each ball and their distances from one another. +// . +// One analagous way to think of metaballs is, consider each "ball" to be a point-light source in +// a dark room. Pick an illumination value, and every point in the volume of the room with +// that intensity of illumination defines the isosurface, which would be a sphere around a +// single source, or a blob surrounding multiple points because the illumination is additive between them. +// . +// Regardless of how you think of it (charge, light, heat, pressure), a stronger metaball +// intensity results in stronger "field" values around the metaball, and correspondingly a +// larger metaball due to the isosurface of a particular value being farther away. +// A metaball is basically a contour surface; that is, a 3D version of a 2D contour. +// . +// Most implementations of metaballs instead use a simple inverse relationship proportional to $1/d$ to +// control how the contributions from each metaball fall off with distance. That +// is the default falloff used for the field types available here. The optional `influence` +// argument is a reciprocal exponent on $d$, defaulting to 1. It controls how much the metaball influences +// others at a given distance. If you set `influence=0.5`, the reciprocal is 2, so you get a $1/d^2$ falloff. +// . +// You can also define your own metaball functions as shown in example 5. +// . +// .h3 Built-in metaball functions +// Various shapes of metaball field density functions are built into this library. You can specify different +// ones for each metaball in the list, and you can also specify your own custom function. +// . +// All built-in functions have these arguments in common: +// * The first argument is always a size (such as a radius or diameter) that determines the size of +// the metaball. Other size arguments may follow as appropriate. All the +// metaball functions are designed so that an isolated metaball with `isovalue=1` appears with a +// radius or size approximately equal to this coefficient, but the metaball can get +// significantly larger when other metaballs are in the bounding box, depending on proximity. +// * `cutoff` - specifies the distance beyond which the metaball has no influence. +// A smooth suppression factor is applied to the metaball's influence on others, starting at half +// the cutoff distance, suppressing the influence to zero at the cutoff distance. Default: INF +// * `influence` - determines the extent of influence of the metaball. This is an inverse +// distance relationship proportional to $1/d$ where $d$ is distance. The `influence` argument is the +// reciprocal of the exponent; for example, If `influence=0.5`, you get an inverse-square falloff +// $1/d^2$, resulting in less influence at a given distance than the default `influence=1`. Setting +// `influence=2` results in a gentle $1/\sqrt d$ falloff, dramatically increasing +// the influence at distances, and you may want to set the `cutoff` argument to limit that influence. +// * `negative` - when true, causes the metaball to have a negative influence on its surroundings. A +// negative metaball can create hollows or dents in other metaballs, or swallow other metaballs +// entirely, making them disappear if the metaball's negative influence is large. A negative +// metaball is never visible directly, only its effect is visible, because the isosurface surrounds +// only field values greater than the isovalue (see Example 2 below). Default: false +// . +// These are the built-in metaball functions. Arguments with default values are optional: +// * `mb_sphere(r/d, cutoff=INF, influence=1, negative=false)` - the standard spherical metaball with a $1/d$ falloff when `influence=1`. The `r` or `d` argument controls the radius or diameter, respectively. For a spherical metaball by itself, you get a sphere of radius `r` at `isovalue=1`. You can create an ellipsoid using `scale()` as the last transformation element in the metaball `funcs` array. +// * `mb_cylinder(r/d, length, cutoff=INF, influence=1, negative=false)` - a cylindrical-shaped field with rounded ends of radius `r` or diameter `d`, useful as a connector. For a single cylindrical metaball by itself at `isovalue=1`, you get a cylinder of radius `r` and straight-side length of `length`, but it grows when other metaballs are nearby. +// * `mb_roundcube(size, squareness=0.5, cutoff=INF, influence=1, negative=false)` - a cuboid metaball with rounded corners that get more rounded at farther distances, depending on isovalue and influence from other metaballs. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5 if omitted. By itself with `isovalue=1`, you get a rounded cube having `size` distance from side to side. +// * `mb_octahedron(r/d, cutoff=INF, influence=1, negative=false)` - an octahedron-shaped metaball with sharp edges and corners, resulting from using [taxicab distance](https://en.wikipedia.org/wiki/Taxicab_geometry) rather than Euclidean distance calculations. By itself with `isovalue=1` you get an octahedron with tip radius of `r` or tip-to-top diameter `d`. +// * `mb_torus(r_maj/d_maj, r_min/d_min, cutoff=INF, influence=1, negative=false)` - a toroidal field oriented perpendicular to the z axis. The arguments `r_maj` and `r_min` control the major and minor radii; otherwise `d_maj` and `d_min` control the major and minor diameters. +// . +// Your own custom function must be written as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// and take `dv` as the first argument with a size as the second argument. `dv` is passed to your +// function as a 3D distance vector from the ball center to the point in the bounding box volume for +// which to calculate the field intensity. The function must return a single number such that higher +// values are enclosed by the metaball and lower values are outside the metaball. +// In this case, if you have written `my_func()`, the array element you initialize must appear +// as `function (dv) my_func(dv, ...)`. See Example 5 below. +// . +// Now for the arguments to this metaball() module or function.... +// Arguments: +// funcs = a 1-D list of transform and function pairs in the form `[trans0, func0, trans1, func1, ...]`, with one pair for each metaball. The transform should be at least a position such as `move([x,y,z])` to specify the location of the metaball center, but you can also include rotations, such as `move([x,y,z])*rot([ax,ay,az])`. You can multiply together any of BOSL2's affine operations like {{xrot()}}, {{scale()}}, and {{skew()}}. This is useful for orienting non-spherical metaballs. The priority order of the transforms is right to left, that is, `move([4,5,6])*rot([45,0,90])` does the rotation first, and then the move, similar to normal OpenSCAD syntax `translate([4,5,6]) rotate([45,0,90]) children()`. +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the metaball surface. **Start with a larger size for experimenting, and refine it gradually.** A small voxel size can significantly slow down processing time, especially with a large `bounding_box`. +// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The voxels needn't fit perfectly inside the bounding box. +// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the coefficient corresponds to the radial size of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 +// --- +// 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 +// convexity = Max number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. +// 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(funcs, voxelsize, boundingbox, isovalue); +// Example(3D,NoAxes): A metaball can be negative. 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]]; +// funcs = [ +// move(centers[0]), mb_sphere(8), +// move(centers[1]), mb_sphere(3, negative=true) +// ]; +// voxelsize = 0.25; +// isovalue = 1; +// boundingbox = [[-7,-6,-6], [3,6,6]]; +// #metaballs(funcs, voxelsize, boundingbox, isovalue); +// color("green") move_copies(centers) sphere(d=1, $fn=16); +// Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. +// funcs = [ +// move([-7,-3,27])*zrot(55), mb_roundcube(6, squareness=1), +// move([5,5,21]), mb_roundcube(5), +// move([10,0,10]), mb_octahedron(5) +// ]; +// voxelsize = 0.5; // a bit slow at this resolution +// boundingbox = [[-12,-9,3], [18,10,32]]; +// metaballs(funcs, voxelsize, boundingbox, isovalue=1); +// Example(3D,NoAxes): Interaction between two tori in different orientations. +// funcs = [ +// move([-10,0,17]), mb_torus(r_maj=6, r_min=2), +// move([7,6,21])*xrot(90), mb_torus(r_maj=7, r_min=3) +// ]; +// voxelsize = 0.5; +// boundingbox = [[-19,-9,9], [18,10,32]]; +// metaballs(funcs, voxelsize, boundingbox, isovalue=1); +// Example(3D,NoAxes,VPD=205,Med): A toy airplane, constructed only from metaball spheres with scaling. The bounding box is used to clip the wingtips, tail, and belly of the fuselage. +// bounding_box = [[-55,-50,-5],[35,50,17]]; +// funcs = [ +// move([-20,0,0])*scale([25,4,4]), mb_sphere(1), // fuselage +// move([30,0,5])*scale([4,0.5,8]), mb_sphere(1), // vertical stabilizer +// move([30,0,0])*scale([4,15,0.5]), mb_sphere(1), // horizontal stabilizer +// move([-15,0,0])*scale([6,45,0.5]), mb_sphere(1) // wing +// ]; +// isovalue = 1; +// voxel_size = 1; +// metaballs(funcs, voxel_size, bounding_box, isovalue); +// Example(3D): Demonstration of a custom metaball function, in this case a sphere with some random noise added to its field potential. The `dv` argument must be first; it is calculated internally as a distance vector from the metaball center to a probe point inside the bounding box, and you convert it to a scalar distance `dist` that is calculated inside your function (`dist` could be a more complicated expression, depending on the shape of the metaball). The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to include the `cutoff` and `influence` arguments in a custom function, but this example shows how. +// function noisy_sphere(dv, r, noise_level, cutoff=INF, influence=1) = +// let( +// noise = rands(0, noise_level, 1)[0], +// dist = norm(dv) + noise +// ) mb_cutoff(dist,cutoff) * (r/dist)^(1/influence); +// +// funcs = [ +// move([-9,0,0]), mb_sphere(5), +// move([9,0,0]), function (dv) noisy_sphere(dv, 5, 0.2), +// ]; +// voxelsize = 0.4; +// boundingbox = [[-16,-8,-8], [16,8,8]]; +// metaballs(funcs, voxelsize, boundingbox, isovalue=1); +// 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. +// include +// tetpts = zrot(15, p = 22 * regular_polyhedron_info("vertices", "tetrahedron")); +// tetxform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; +// +// funcs = [ +// // vertical cylinder arm +// move([0,0,15]), mb_cylinder(2, 17, influence=0.8), +// // ellipsoid arms +// tetxform[0], mb_sphere(1, cutoff=30), +// tetxform[1], mb_sphere(1, cutoff=30), +// tetxform[2], mb_sphere(1, cutoff=30), +// // ring on top +// move([0,0,35])*xrot(90), mb_torus(r_maj=8, r_min=2.5, cutoff=35), +// // feet +// move(2.2*tetpts[0]), mb_sphere(5, cutoff=30), +// move(2.2*tetpts[1]), mb_sphere(5, cutoff=30), +// move(2.2*tetpts[2]), mb_sphere(5, cutoff=30) +// ]; +// voxelsize = 1; +// boundingbox = [[-22,-32,-13], [36,32,46]]; +// // useful to save as VNF for copies and manipulations +// vnf = metaballs(funcs, voxelsize, boundingbox, isovalue=1); +// vnf_polyhedron(vnf); + +module metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false) { + vnf = metaballs(funcs, voxel_size, bounding_box, isovalue, closed, show_stats); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); +} + +function metaballs(funcs, voxel_size, bounding_box, isovalue=1, 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( + nballs = len(funcs)/2, + // set up transformation matrices in advance + transmatrix = [ + for(i=[0:nballs-1]) + let(j=2*i) + assert(is_matrix(funcs[j],4,4), str("\nfuncs entry at position ", j, " must be a 4×4 matrix.")) + assert(is_function(funcs[j+1]), str("\nfuncs entry at position ", j+1, " must be a function literal.")) + transpose(select(matrix_inverse(funcs[j]), 0,2)) + ], + + // set up field array + bot = bounding_box[0], + top = bounding_box[1], + halfvox = 0.5*voxel_size, + // accumulate metaball contributions using matrices rather than sums + xset = [bot.x:voxel_size:top.x+halfvox], + yset = list([bot.y:voxel_size:top.y+halfvox]), + zset = list([bot.z:voxel_size:top.z+halfvox]), + allpts = [for(x=xset, y=yset, z=zset) [x,y,z,1]], + trans_pts = [for(i=[0:nballs-1]) allpts*transmatrix[i]], + allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funcs[2*i+1](pt)]], + total = _sum(allvals,allvals[0]*0), + fieldarray = list_to_matrix(list_to_matrix(total,len(zset)),len(yset)) + ) isosurface(fieldarray, isovalue, voxel_size, closed=closed, show_stats=show_stats, _origin=bounding_box[0]); + + +/// ---------- isosurface stuff starts here ---------- + +// Function&Module: isosurface() +// Synopsis: Creates a 3D isosurface. +// SynTags: Geom,VNF +// Topics: Isosurfaces, VNF Generators +// Usage: As a module +// isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = isosurface(f, isovalue, voxel_size, bounding_box, [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 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. +// . +// A [marching cubes](https://en.wikipedia.org/wiki/Marching_cubes) algorithm is used +// to identify an envelope containing the isosurface within the bounding box. The surface +// intersecttion with a voxel cube is then triangulated to form a surface fragment, which is +// combined with all other surface fragments. Ambiguities in triangulating the surfaces +// in certain voxel cube configurations are resolved so that all triangular facets are +// properly oriented with no holes in the surface. If a side of the bounding box clips +// the isosurface, this clipped area is filled in so that the surface remains manifold. +// . +// Be mindful of how you set `voxel_size` and `bounding_box`. For example a voxel size +// of 1 unit with a bounding box volume of 200×200×200 may be noticeably slow, +// requiring calculation and storage of 8,000,000 field values, and more processing +// and memory to generate the triangulated mesh. On the other hand, a voxel size of 5 +// in a 100×100×100 bounding box requires only 8,000 field values and the mesh +// generates fairly quickly, just a handful of seconds. A good rule is to keep the +// number of field values below 10,000 for preview, and adjust the voxel size +// smaller for final rendering. If the isosurface fits completely within the bounding +// box, you can call {{pointlist_bounds()}} on `vnf[0]` returned from the +// `isosurface()` function to get an idea of a more optimal smaller bounding box to use, +// possibly allowing increasing resolution by decresing the voxel size. You can also set +// the parameter `show_stats=true` to get the bounds of the voxels containing the surface. +// . +// 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 (often +// resulting from clipping by the bounding box) 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: +// f = The isosurface function. Can be a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) taking as input the `x,y,z` grid coordinates and returning a single value, or a 3D array (all points in the grid precomputed). +// **As a function literal:** 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)`. +// **As an array:** The array you pass in should be organized so that the indices are in order of `[x][y][z]` when the array is referenced; that is, `f[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. +// 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. +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. +// bounding_box = Applicable only when `f` is a function literal. This is 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. Default: undef +// --- +// reverse = When true, reverses the orientation of the facets in the mesh. Default: false +// 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 +// convexity = Max number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = 0; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox, +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = 0; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox); +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox], +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox); +// Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). +// function schwartz_p(x,y,z, wavelength) = let( +// p = 360/wavelength, +// px = p*x, py = p*y, pz = p*z +// ) cos(px) + cos(py) + cos(pz); +// isovalue = [-0.2, 0.2]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) schwartz_p(x,y,z, 100), +// isovalue, voxel_size=4, bounding_box=bbox); +// Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). +// function neovius(x,y,z, wavelength) = let( +// p = 360/wavelength, +// px = p*x, py = p*y, pz = p*z +// ) 3*(cos(px) + cos(py) + cos(pz)) + 4*cos(px)*cos(py)*cos(pz); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) neovius(x,y,z,200), +// isovalue, voxel_size=4, bounding_box=bbox); +// Example(3D): Using an array for the `f` argument instead of a function literal. {{metaballs()}} also makes use of this feature, calculating the the 3D grid first. +// field = [ +// repeat(0,[6,6]), +// [ [0,1,2,2,1,0], +// [1,2,3,3,2,1], +// [2,3,4,4,3,2], +// [2,3,4,4,3,2], +// [1,2,3,3,2,1], +// [0,1,2,2,1,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,1,1,0,0], +// [0,2,3,3,2,0], +// [0,2,3,3,2,0], +// [0,0,1,1,0,0], +// [0,0,0,0,0,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,0,0,0,0], +// [0,1,2,2,1,0], +// [0,1,2,2,1,0], +// [0,0,0,0,0,0], +// [0,0,0,0,0,0] +// ], +// repeat(0,[6,6]) +// ]; +// rotate([0,-90,180]) +// isosurface_array(field, isovalue=0.5, +// voxel_size=10); + +module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _origin=undef) { + vnf = isosurface(f, isovalue, bounding_box, voxel_size, reverse, closed, show_stats, _origin); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); +} + +function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, show_stats=false, _origin=undef) = + assert(all_defined([f, isovalue, voxel_size]), "\nThe parameters f, isovalue, and bounding_box must all be defined.") + assert((is_function(f) && is_def(bounding_box)) || (is_list(f) && is_undef(bounding_box)), + "\nbounding_box must be passed if f is a function, and cannot be passed if f is an array.") + let( + isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, + isovalmax = is_list(isovalue) ? isovalue[1] : INF, + bbox = is_function(f) + ? let( // new bounding box quantized for voxel_size + hv = 0.5*voxel_size, + 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] + : let( + nx = len(f)-1, + ny = len(f[0])-1, + nz = len(f[0][0])-1 + ) is_def(_origin) ? [_origin, _origin+voxel_size*[nx,ny,nz]] + : [-0.5*voxel_size*[nx,ny,nz], 0.5*voxel_size*[nx, ny, nz]], + cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=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), + faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], + dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 +) [trianglepoints, faces]; +>>>>>>> Stashed changes From 95e7a8607c5b78da18c658201ff8cbe1d792b088 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 5 Feb 2025 11:59:29 -0800 Subject: [PATCH 03/11] Update isosurface.scad --- isosurface.scad | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/isosurface.scad b/isosurface.scad index 84f6ae28..dc6ab97d 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,4 +1,5 @@ <<<<<<< Updated upstream +<<<<<<< Updated upstream ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // An isosurface is a three-dimensional surface representing points of a constant @@ -1413,6 +1414,8 @@ for(i=[0:nballs-1]) let( ) 0 ]; ======= +======= +>>>>>>> Stashed changes ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), @@ -2894,4 +2897,7 @@ function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 ) [trianglepoints, faces]; +<<<<<<< Updated upstream +>>>>>>> Stashed changes +======= >>>>>>> Stashed changes From 76da2a2fc7438f4688a2b831ecc0c24c3109b5e0 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 5 Feb 2025 12:12:54 -0800 Subject: [PATCH 04/11] Update isosurface.scad --- isosurface.scad | 1422 ----------------------------------------------- 1 file changed, 1422 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index dc6ab97d..148b5a7b 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,1421 +1,3 @@ -<<<<<<< Updated upstream -<<<<<<< Updated upstream -///////////////////////////////////////////////////////////////////// -// 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. -// 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. -// . -// 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. -// -// Includes: -// include -// include -// FileGroup: Advanced Modeling -// FileSummary: Isosurfaces and metaballs. -////////////////////////////////////////////////////////////////////// - - -/* -Lookup Tables for Transvoxel's Modified Marching Cubes - -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. - -Axes are - z - (top) - | y (back) - | / - |/ - +----- x (right) - -Vertex and edge layout (heavier = and # indicate closer to viewer): - - 3 +----------+ 7 +----10----+ - /: /| /: /| - / : / | 1 2 5 6 - 1 +==========+5 | +=====9====+ | - # 2+ - - - # -+ 6 # +- - 11-# -+ - # / # / 0 3 4 7 - #/ #/ #/ #/ - 0 +==========+ 4 +=====8=====+ - -z changes fastest, then y, then x - ------------------------------------------------------------ -Addition by Alex Matulich: -Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. - - 4(back) - 3 +----------+ 7 - /: 5(top) /| - / : / | - 1 +==========+5 | <-- 3 (side) -0(side) --> # 2+ - - - # -+ 6 - # / # / - #/ 2(bot) #/ - 0 +----------+ 4 - 1(front) -*/ - -/// four indices for each face of the cube, counterclockwise looking from inside out -_MCFaceVertexIndices = [ - [], - [0,2,3,1], // left, x=0 plane - [0,1,5,4], // front, y=0 plane - [0,4,6,2], // bottom, z=0 plane - [4,5,7,6], // right, x=voxsize plane - [2,6,7,3], // back, y=voxsize plane - [1,3,7,5], // top, z=voxsize plane -]; - -/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. -function _bbox_faces(v0, voxsize, bbox) = let( - a = v0-bbox[0], - bb1 = bbox[1] - [voxsize,voxsize,voxsize], - b = v0-bb1 -) [ - if(a[0]==0) 1, - if(a[1]==0) 2, - if(a[2]==0) 3, - if(b[0]>=0) 4, - if(b[1]>=0) 5, - if(b[2]>=0) 6 -]; -/// End of bounding-box faace-clipping stuff. Back to the marching cubes triangulation.... - - -/// Pair of vertex indices for each edge on the voxel -_MCEdgeVertexIndices = [ - [0, 1], - [1, 3], - [3, 2], - [2, 0], - [4, 5], - [5, 7], - [7, 6], - [6, 4], - [0, 4], - [1, 5], - [3, 7], - [2, 6], -]; - -/// For each of the 255 configurations of a marching cube, define a list of triangles, specified as triples of edge indices. -_MCTriangleTable = [ - [], - [3,8,0], - [1,0,9], - [9,1,8,8,1,3], - [3,2,11], - [2,11,0,0,11,8], - [1,0,9,3,2,11], - [11,1,2,11,9,1,11,8,9], - [10,2,1], - [2,1,10,0,3,8], - [0,9,2,2,9,10], - [8,2,3,8,10,2,8,9,10], - [1,10,3,3,10,11], - [10,0,1,10,8,0,10,11,8], - [9,3,0,9,11,3,9,10,11], - [9,10,8,8,10,11], - [7,4,8], - [0,3,4,4,3,7], - [0,9,1,4,8,7], - [1,4,9,1,7,4,1,3,7], - [11,3,2,8,7,4], - [4,11,7,4,2,11,4,0,2], - [3,2,11,0,9,1,4,8,7], - [9,1,4,4,1,7,7,1,2,7,2,11], - [7,4,8,1,10,2], - [7,4,3,3,4,0,10,2,1], - [10,2,9,9,2,0,7,4,8], - [7,4,9,7,9,2,9,10,2,3,7,2], - [1,10,3,3,10,11,4,8,7], - [4,0,7,0,1,10,7,0,10,7,10,11], - [7,4,8,9,3,0,9,11,3,9,10,11], - [7,4,11,4,9,11,9,10,11], - [5,9,4], - [8,0,3,9,4,5], - [1,0,5,5,0,4], - [5,8,4,5,3,8,5,1,3], - [3,2,11,5,9,4], - [2,11,0,0,11,8,5,9,4], - [4,5,0,0,5,1,11,3,2], - [11,8,2,8,4,5,2,8,5,2,5,1], - [5,9,4,1,10,2], - [0,3,8,1,10,2,5,9,4], - [2,5,10,2,4,5,2,0,4], - [4,5,8,8,5,3,3,5,10,3,10,2], - [11,3,10,10,3,1,4,5,9], - [4,5,9,10,0,1,10,8,0,10,11,8], - [4,5,10,4,10,3,10,11,3,0,4,3], - [4,5,8,5,10,8,10,11,8], - [5,9,7,7,9,8], - [3,9,0,3,5,9,3,7,5], - [7,0,8,7,1,0,7,5,1], - [3,7,1,1,7,5], - [5,9,7,7,9,8,2,11,3], - [5,9,0,5,0,11,0,2,11,7,5,11], - [2,11,3,7,0,8,7,1,0,7,5,1], - [2,11,1,11,7,1,7,5,1], - [8,7,9,9,7,5,2,1,10], - [10,2,1,3,9,0,3,5,9,3,7,5], - [2,0,10,0,8,7,10,0,7,10,7,5], - [10,2,5,2,3,5,3,7,5], - [5,9,8,5,8,7,1,10,3,10,11,3], - [1,10,0,0,10,11,0,11,7,0,7,5,0,5,9], - [8,7,0,0,7,5,0,5,10,0,10,11,0,11,3], - [5,11,7,10,11,5], - [11,6,7], - [3,8,0,7,11,6], - [1,0,9,7,11,6], - [9,1,8,8,1,3,6,7,11], - [6,7,2,2,7,3], - [0,7,8,0,6,7,0,2,6], - [6,7,2,2,7,3,9,1,0], - [9,1,2,9,2,7,2,6,7,8,9,7], - [10,2,1,11,6,7], - [2,1,10,3,8,0,7,11,6], - [0,9,2,2,9,10,7,11,6], - [6,7,11,8,2,3,8,10,2,8,9,10], - [7,10,6,7,1,10,7,3,1], - [1,10,0,0,10,8,8,10,6,8,6,7], - [9,10,0,10,6,7,0,10,7,0,7,3], - [6,7,10,7,8,10,8,9,10], - [4,8,6,6,8,11], - [6,3,11,6,0,3,6,4,0], - [11,6,8,8,6,4,1,0,9], - [6,4,11,4,9,1,11,4,1,11,1,3], - [2,8,3,2,4,8,2,6,4], - [0,2,4,4,2,6], - [9,1,0,2,8,3,2,4,8,2,6,4], - [9,1,4,1,2,4,2,6,4], - [4,8,6,6,8,11,1,10,2], - [1,10,2,6,3,11,6,0,3,6,4,0], - [0,9,10,0,10,2,4,8,6,8,11,6], - [11,6,3,3,6,4,3,4,9,3,9,10,3,10,2], - [1,10,6,1,6,8,6,4,8,3,1,8], - [1,10,0,10,6,0,6,4,0], - [0,9,3,3,9,10,3,10,6,3,6,4,3,4,8], - [4,10,6,9,10,4], - [4,5,9,6,7,11], - [7,11,6,8,0,3,9,4,5], - [1,0,5,5,0,4,11,6,7], - [11,6,7,5,8,4,5,3,8,5,1,3], - [3,2,7,7,2,6,9,4,5], - [5,9,4,0,7,8,0,6,7,0,2,6], - [1,0,4,1,4,5,3,2,7,2,6,7], - [4,5,8,8,5,1,8,1,2,8,2,6,8,6,7], - [6,7,11,5,9,4,1,10,2], - [5,9,4,7,11,6,0,3,8,2,1,10], - [7,11,6,2,5,10,2,4,5,2,0,4], - [6,7,11,3,8,4,3,4,5,3,5,2,2,5,10], - [9,4,5,7,10,6,7,1,10,7,3,1], - [5,9,4,8,0,1,8,1,10,8,10,7,7,10,6], - [6,7,10,10,7,3,10,3,0,10,0,4,10,4,5], - [4,5,8,8,5,10,8,10,6,8,6,7], - [9,6,5,9,11,6,9,8,11], - [0,3,9,9,3,5,5,3,11,5,11,6], - [1,0,8,1,8,6,8,11,6,5,1,6], - [11,6,3,6,5,3,5,1,3], - [2,6,3,6,5,9,3,6,9,3,9,8], - [5,9,6,9,0,6,0,2,6], - [3,2,8,8,2,6,8,6,5,8,5,1,8,1,0], - [1,6,5,2,6,1], - [2,1,10,9,6,5,9,11,6,9,8,11], - [2,1,10,5,9,0,5,0,3,5,3,6,6,3,11], - [10,2,5,5,2,0,5,0,8,5,8,11,5,11,6], - [10,2,5,5,2,3,5,3,11,5,11,6], - [5,9,6,6,9,8,6,8,3,6,3,1,6,1,10], - [5,9,6,6,9,0,6,0,1,6,1,10], - [8,3,0,5,10,6], - [6,5,10], - [6,10,5], - [3,8,0,5,6,10], - [9,1,0,10,5,6], - [3,8,1,1,8,9,6,10,5], - [6,10,5,2,11,3], - [8,0,11,11,0,2,5,6,10], - [10,5,6,1,0,9,3,2,11], - [5,6,10,11,1,2,11,9,1,11,8,9], - [2,1,6,6,1,5], - [5,6,1,1,6,2,8,0,3], - [6,9,5,6,0,9,6,2,0], - [8,9,3,9,5,6,3,9,6,3,6,2], - [3,6,11,3,5,6,3,1,5], - [5,6,11,5,11,0,11,8,0,1,5,0], - [0,9,3,3,9,11,11,9,5,11,5,6], - [5,6,9,6,11,9,11,8,9], - [7,4,8,5,6,10], - [0,3,4,4,3,7,10,5,6], - [4,8,7,9,1,0,10,5,6], - [6,10,5,1,4,9,1,7,4,1,3,7], - [11,3,2,7,4,8,5,6,10], - [10,5,6,4,11,7,4,2,11,4,0,2], - [7,4,8,3,2,11,9,1,0,10,5,6], - [10,5,6,7,4,9,7,9,1,7,1,11,11,1,2], - [2,1,6,6,1,5,8,7,4], - [7,4,0,7,0,3,5,6,1,6,2,1], - [8,7,4,6,9,5,6,0,9,6,2,0], - [5,6,9,9,6,2,9,2,3,9,3,7,9,7,4], - [4,8,7,3,6,11,3,5,6,3,1,5], - [7,4,11,11,4,0,11,0,1,11,1,5,11,5,6], - [4,8,7,11,3,0,11,0,9,11,9,6,6,9,5], - [5,6,9,9,6,11,9,11,7,9,7,4], - [9,4,10,10,4,6], - [6,10,4,4,10,9,3,8,0], - [0,10,1,0,6,10,0,4,6], - [3,8,4,3,4,10,4,6,10,1,3,10], - [9,4,10,10,4,6,3,2,11], - [8,0,2,8,2,11,9,4,10,4,6,10], - [11,3,2,0,10,1,0,6,10,0,4,6], - [2,11,1,1,11,8,1,8,4,1,4,6,1,6,10], - [4,1,9,4,2,1,4,6,2], - [3,8,0,4,1,9,4,2,1,4,6,2], - [4,6,0,0,6,2], - [3,8,2,8,4,2,4,6,2], - [3,1,11,1,9,4,11,1,4,11,4,6], - [9,4,1,1,4,6,1,6,11,1,11,8,1,8,0], - [11,3,6,3,0,6,0,4,6], - [8,6,11,4,6,8], - [10,7,6,10,8,7,10,9,8], - [10,9,6,9,0,3,6,9,3,6,3,7], - [8,7,0,0,7,1,1,7,6,1,6,10], - [6,10,7,10,1,7,1,3,7], - [3,2,11,10,7,6,10,8,7,10,9,8], - [6,10,7,7,10,9,7,9,0,7,0,2,7,2,11], - [11,3,2,1,0,8,1,8,7,1,7,10,10,7,6], - [6,10,7,7,10,1,7,1,2,7,2,11], - [8,7,6,8,6,1,6,2,1,9,8,1], - [0,3,9,9,3,7,9,7,6,9,6,2,9,2,1], - [8,7,0,7,6,0,6,2,0], - [7,2,3,6,2,7], - [11,3,6,6,3,1,6,1,9,6,9,8,6,8,7], - [11,7,6,1,9,0], - [11,3,6,6,3,0,6,0,8,6,8,7], - [11,7,6], - [10,5,11,11,5,7], - [10,5,11,11,5,7,0,3,8], - [7,11,5,5,11,10,0,9,1], - [3,8,9,3,9,1,7,11,5,11,10,5], - [5,2,10,5,3,2,5,7,3], - [0,2,8,2,10,5,8,2,5,8,5,7], - [0,9,1,5,2,10,5,3,2,5,7,3], - [10,5,2,2,5,7,2,7,8,2,8,9,2,9,1], - [1,11,2,1,7,11,1,5,7], - [8,0,3,1,11,2,1,7,11,1,5,7], - [0,9,5,0,5,11,5,7,11,2,0,11], - [3,8,2,2,8,9,2,9,5,2,5,7,2,7,11], - [5,7,1,1,7,3], - [8,0,7,0,1,7,1,5,7], - [0,9,3,9,5,3,5,7,3], - [9,7,8,5,7,9], - [8,5,4,8,10,5,8,11,10], - [10,5,4,10,4,3,4,0,3,11,10,3], - [1,0,9,8,5,4,8,10,5,8,11,10], - [9,1,4,4,1,3,4,3,11,4,11,10,4,10,5], - [10,5,2,2,5,3,3,5,4,3,4,8], - [10,5,2,5,4,2,4,0,2], - [9,1,0,3,2,10,3,10,5,3,5,8,8,5,4], - [10,5,2,2,5,4,2,4,9,2,9,1], - [1,5,2,5,4,8,2,5,8,2,8,11], - [2,1,11,11,1,5,11,5,4,11,4,0,11,0,3], - [4,8,5,5,8,11,5,11,2,5,2,0,5,0,9], - [5,4,9,2,3,11], - [4,8,5,8,3,5,3,1,5], - [0,5,4,1,5,0], - [0,9,3,3,9,5,3,5,4,3,4,8], - [5,4,9], - [11,4,7,11,9,4,11,10,9], - [0,3,8,11,4,7,11,9,4,11,10,9], - [0,4,1,4,7,11,1,4,11,1,11,10], - [7,11,4,4,11,10,4,10,1,4,1,3,4,3,8], - [9,4,7,9,7,2,7,3,2,10,9,2], - [8,0,7,7,0,2,7,2,10,7,10,9,7,9,4], - [1,0,10,10,0,4,10,4,7,10,7,3,10,3,2], - [7,8,4,10,1,2], - [9,4,1,1,4,2,2,4,7,2,7,11], - [8,0,3,2,1,9,2,9,4,2,4,11,11,4,7], - [7,11,4,11,2,4,2,0,4], - [3,8,2,2,8,4,2,4,7,2,7,11], - [9,4,1,4,7,1,7,3,1], - [9,4,1,1,4,7,1,7,8,1,8,0], - [3,4,7,0,4,3], - [7,8,4], - [8,11,9,9,11,10], - [0,3,9,3,11,9,11,10,9], - [1,0,10,0,8,10,8,11,10], - [10,3,11,1,3,10], - [3,2,8,2,10,8,10,9,8], - [9,2,10,0,2,9], - [1,0,10,10,0,8,10,8,3,10,3,2], - [2,10,1], - [2,1,11,1,9,11,9,8,11], - [2,1,11,11,1,9,11,9,0,11,0,3], - [11,0,8,2,0,11], - [3,11,2], - [1,8,3,9,8,1], - [1,9,0], - [8,3,0], - [], -]; - -/// Same list as above, but with each row in reverse order. Needed for generating shells (two isosurfaces at slightly different iso values). -/// More efficient just to have a static table than to generate it each time by calling reverse() hundreds of times (although this static table was generated that way). -_MCTriangleTable_reverse = [ - [], - [0,8,3], - [9,0,1], - [3,1,8,8,1,9], - [11,2,3], - [8,11,0,0,11,2], - [11,2,3,9,0,1], - [9,8,11,1,9,11,2,1,11], - [1,2,10], - [8,3,0,10,1,2], - [10,9,2,2,9,0], - [10,9,8,2,10,8,3,2,8], - [11,10,3,3,10,1], - [8,11,10,0,8,10,1,0,10], - [11,10,9,3,11,9,0,3,9], - [11,10,8,8,10,9], - [8,4,7], - [7,3,4,4,3,0], - [7,8,4,1,9,0], - [7,3,1,4,7,1,9,4,1], - [4,7,8,2,3,11], - [2,0,4,11,2,4,7,11,4], - [7,8,4,1,9,0,11,2,3], - [11,2,7,2,1,7,7,1,4,4,1,9], - [2,10,1,8,4,7], - [1,2,10,0,4,3,3,4,7], - [8,4,7,0,2,9,9,2,10], - [2,7,3,2,10,9,2,9,7,9,4,7], - [7,8,4,11,10,3,3,10,1], - [11,10,7,10,0,7,10,1,0,7,0,4], - [11,10,9,3,11,9,0,3,9,8,4,7], - [11,10,9,11,9,4,11,4,7], - [4,9,5], - [5,4,9,3,0,8], - [4,0,5,5,0,1], - [3,1,5,8,3,5,4,8,5], - [4,9,5,11,2,3], - [4,9,5,8,11,0,0,11,2], - [2,3,11,1,5,0,0,5,4], - [1,5,2,5,8,2,5,4,8,2,8,11], - [2,10,1,4,9,5], - [4,9,5,2,10,1,8,3,0], - [4,0,2,5,4,2,10,5,2], - [2,10,3,10,5,3,3,5,8,8,5,4], - [9,5,4,1,3,10,10,3,11], - [8,11,10,0,8,10,1,0,10,9,5,4], - [3,4,0,3,11,10,3,10,4,10,5,4], - [8,11,10,8,10,5,8,5,4], - [8,9,7,7,9,5], - [5,7,3,9,5,3,0,9,3], - [1,5,7,0,1,7,8,0,7], - [5,7,1,1,7,3], - [3,11,2,8,9,7,7,9,5], - [11,5,7,11,2,0,11,0,5,0,9,5], - [1,5,7,0,1,7,8,0,7,3,11,2], - [1,5,7,1,7,11,1,11,2], - [10,1,2,5,7,9,9,7,8], - [5,7,3,9,5,3,0,9,3,1,2,10], - [5,7,10,7,0,10,7,8,0,10,0,2], - [5,7,3,5,3,2,5,2,10], - [3,11,10,3,10,1,7,8,5,8,9,5], - [9,5,0,5,7,0,7,11,0,11,10,0,0,10,1], - [3,11,0,11,10,0,10,5,0,5,7,0,0,7,8], - [5,11,10,7,11,5], - [7,6,11], - [6,11,7,0,8,3], - [6,11,7,9,0,1], - [11,7,6,3,1,8,8,1,9], - [3,7,2,2,7,6], - [6,2,0,7,6,0,8,7,0], - [0,1,9,3,7,2,2,7,6], - [7,9,8,7,6,2,7,2,9,2,1,9], - [7,6,11,1,2,10], - [6,11,7,0,8,3,10,1,2], - [6,11,7,10,9,2,2,9,0], - [10,9,8,2,10,8,3,2,8,11,7,6], - [1,3,7,10,1,7,6,10,7], - [7,6,8,6,10,8,8,10,0,0,10,1], - [3,7,0,7,10,0,7,6,10,0,10,9], - [10,9,8,10,8,7,10,7,6], - [11,8,6,6,8,4], - [0,4,6,3,0,6,11,3,6], - [9,0,1,4,6,8,8,6,11], - [3,1,11,1,4,11,1,9,4,11,4,6], - [4,6,2,8,4,2,3,8,2], - [6,2,4,4,2,0], - [4,6,2,8,4,2,3,8,2,0,1,9], - [4,6,2,4,2,1,4,1,9], - [2,10,1,11,8,6,6,8,4], - [0,4,6,3,0,6,11,3,6,2,10,1], - [6,11,8,6,8,4,2,10,0,10,9,0], - [2,10,3,10,9,3,9,4,3,4,6,3,3,6,11], - [8,1,3,8,4,6,8,6,1,6,10,1], - [0,4,6,0,6,10,0,10,1], - [8,4,3,4,6,3,6,10,3,10,9,3,3,9,0], - [4,10,9,6,10,4], - [11,7,6,9,5,4], - [5,4,9,3,0,8,6,11,7], - [7,6,11,4,0,5,5,0,1], - [3,1,5,8,3,5,4,8,5,7,6,11], - [5,4,9,6,2,7,7,2,3], - [6,2,0,7,6,0,8,7,0,4,9,5], - [7,6,2,7,2,3,5,4,1,4,0,1], - [7,6,8,6,2,8,2,1,8,1,5,8,8,5,4], - [2,10,1,4,9,5,11,7,6], - [10,1,2,8,3,0,6,11,7,4,9,5], - [4,0,2,5,4,2,10,5,2,6,11,7], - [10,5,2,2,5,3,5,4,3,4,8,3,11,7,6], - [1,3,7,10,1,7,6,10,7,5,4,9], - [6,10,7,7,10,8,10,1,8,1,0,8,4,9,5], - [5,4,10,4,0,10,0,3,10,3,7,10,10,7,6], - [7,6,8,6,10,8,10,5,8,8,5,4], - [11,8,9,6,11,9,5,6,9], - [6,11,5,11,3,5,5,3,9,9,3,0], - [6,1,5,6,11,8,6,8,1,8,0,1], - [3,1,5,3,5,6,3,6,11], - [8,9,3,9,6,3,9,5,6,3,6,2], - [6,2,0,6,0,9,6,9,5], - [0,1,8,1,5,8,5,6,8,6,2,8,8,2,3], - [1,6,2,5,6,1], - [11,8,9,6,11,9,5,6,9,10,1,2], - [11,3,6,6,3,5,3,0,5,0,9,5,10,1,2], - [6,11,5,11,8,5,8,0,5,0,2,5,5,2,10], - [6,11,5,11,3,5,3,2,5,5,2,10], - [10,1,6,1,3,6,3,8,6,8,9,6,6,9,5], - [10,1,6,1,0,6,0,9,6,6,9,5], - [6,10,5,0,3,8], - [10,5,6], - [5,10,6], - [10,6,5,0,8,3], - [6,5,10,0,1,9], - [5,10,6,9,8,1,1,8,3], - [3,11,2,5,10,6], - [10,6,5,2,0,11,11,0,8], - [11,2,3,9,0,1,6,5,10], - [9,8,11,1,9,11,2,1,11,10,6,5], - [5,1,6,6,1,2], - [3,0,8,2,6,1,1,6,5], - [0,2,6,9,0,6,5,9,6], - [2,6,3,6,9,3,6,5,9,3,9,8], - [5,1,3,6,5,3,11,6,3], - [0,5,1,0,8,11,0,11,5,11,6,5], - [6,5,11,5,9,11,11,9,3,3,9,0], - [9,8,11,9,11,6,9,6,5], - [10,6,5,8,4,7], - [6,5,10,7,3,4,4,3,0], - [6,5,10,0,1,9,7,8,4], - [7,3,1,4,7,1,9,4,1,5,10,6], - [10,6,5,8,4,7,2,3,11], - [2,0,4,11,2,4,7,11,4,6,5,10], - [6,5,10,0,1,9,11,2,3,8,4,7], - [2,1,11,11,1,7,1,9,7,9,4,7,6,5,10], - [4,7,8,5,1,6,6,1,2], - [1,2,6,1,6,5,3,0,7,0,4,7], - [0,2,6,9,0,6,5,9,6,4,7,8], - [4,7,9,7,3,9,3,2,9,2,6,9,9,6,5], - [5,1,3,6,5,3,11,6,3,7,8,4], - [6,5,11,5,1,11,1,0,11,0,4,11,11,4,7], - [5,9,6,6,9,11,9,0,11,0,3,11,7,8,4], - [4,7,9,7,11,9,11,6,9,9,6,5], - [6,4,10,10,4,9], - [0,8,3,9,10,4,4,10,6], - [6,4,0,10,6,0,1,10,0], - [10,3,1,10,6,4,10,4,3,4,8,3], - [11,2,3,6,4,10,10,4,9], - [10,6,4,10,4,9,11,2,8,2,0,8], - [6,4,0,10,6,0,1,10,0,2,3,11], - [10,6,1,6,4,1,4,8,1,8,11,1,1,11,2], - [2,6,4,1,2,4,9,1,4], - [2,6,4,1,2,4,9,1,4,0,8,3], - [2,6,0,0,6,4], - [2,6,4,2,4,8,2,8,3], - [6,4,11,4,1,11,4,9,1,11,1,3], - [0,8,1,8,11,1,11,6,1,6,4,1,1,4,9], - [6,4,0,6,0,3,6,3,11], - [8,6,4,11,6,8], - [8,9,10,7,8,10,6,7,10], - [7,3,6,3,9,6,3,0,9,6,9,10], - [10,6,1,6,7,1,1,7,0,0,7,8], - [7,3,1,7,1,10,7,10,6], - [8,9,10,7,8,10,6,7,10,11,2,3], - [11,2,7,2,0,7,0,9,7,9,10,7,7,10,6], - [6,7,10,10,7,1,7,8,1,8,0,1,2,3,11], - [11,2,7,2,1,7,1,10,7,7,10,6], - [1,8,9,1,2,6,1,6,8,6,7,8], - [1,2,9,2,6,9,6,7,9,7,3,9,9,3,0], - [0,2,6,0,6,7,0,7,8], - [7,2,6,3,2,7], - [7,8,6,8,9,6,9,1,6,1,3,6,6,3,11], - [0,9,1,6,7,11], - [7,8,6,8,0,6,0,3,6,6,3,11], - [6,7,11], - [7,5,11,11,5,10], - [8,3,0,7,5,11,11,5,10], - [1,9,0,10,11,5,5,11,7], - [5,10,11,5,11,7,1,9,3,9,8,3], - [3,7,5,2,3,5,10,2,5], - [7,5,8,5,2,8,5,10,2,8,2,0], - [3,7,5,2,3,5,10,2,5,1,9,0], - [1,9,2,9,8,2,8,7,2,7,5,2,2,5,10], - [7,5,1,11,7,1,2,11,1], - [7,5,1,11,7,1,2,11,1,3,0,8], - [11,0,2,11,7,5,11,5,0,5,9,0], - [11,7,2,7,5,2,5,9,2,9,8,2,2,8,3], - [3,7,1,1,7,5], - [7,5,1,7,1,0,7,0,8], - [3,7,5,3,5,9,3,9,0], - [9,7,5,8,7,9], - [10,11,8,5,10,8,4,5,8], - [3,10,11,3,0,4,3,4,10,4,5,10], - [10,11,8,5,10,8,4,5,8,9,0,1], - [5,10,4,10,11,4,11,3,4,3,1,4,4,1,9], - [8,4,3,4,5,3,3,5,2,2,5,10], - [2,0,4,2,4,5,2,5,10], - [4,5,8,8,5,3,5,10,3,10,2,3,0,1,9], - [1,9,2,9,4,2,4,5,2,2,5,10], - [11,8,2,8,5,2,8,4,5,2,5,1], - [3,0,11,0,4,11,4,5,11,5,1,11,11,1,2], - [9,0,5,0,2,5,2,11,5,11,8,5,5,8,4], - [11,3,2,9,4,5], - [5,1,3,5,3,8,5,8,4], - [0,5,1,4,5,0], - [8,4,3,4,5,3,5,9,3,3,9,0], - [9,4,5], - [9,10,11,4,9,11,7,4,11], - [9,10,11,4,9,11,7,4,11,8,3,0], - [10,11,1,11,4,1,11,7,4,1,4,0], - [8,3,4,3,1,4,1,10,4,10,11,4,4,11,7], - [2,9,10,2,3,7,2,7,9,7,4,9], - [4,9,7,9,10,7,10,2,7,2,0,7,7,0,8], - [2,3,10,3,7,10,7,4,10,4,0,10,10,0,1], - [2,1,10,4,8,7], - [11,7,2,7,4,2,2,4,1,1,4,9], - [7,4,11,11,4,2,4,9,2,9,1,2,3,0,8], - [4,0,2,4,2,11,4,11,7], - [11,7,2,7,4,2,4,8,2,2,8,3], - [1,3,7,1,7,4,1,4,9], - [0,8,1,8,7,1,7,4,1,1,4,9], - [3,4,0,7,4,3], - [4,8,7], - [10,11,9,9,11,8], - [9,10,11,9,11,3,9,3,0], - [10,11,8,10,8,0,10,0,1], - [10,3,1,11,3,10], - [8,9,10,8,10,2,8,2,3], - [9,2,0,10,2,9], - [2,3,10,3,8,10,8,0,10,10,0,1], - [1,10,2], - [11,8,9,11,9,1,11,1,2], - [3,0,11,0,9,11,9,1,11,11,1,2], - [11,0,2,8,0,11], - [2,11,3], - [1,8,9,3,8,1], - [0,9,1], - [0,3,8], - [] -]; - - -// Function&Module: isosurface() -// Synopsis: Creates a 3D isosurface. -// 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=]); -// Usage: As a function -// vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, [additional=], [close_clip=], [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. -// . -// A [marching cubes](https://en.wikipedia.org/wiki/Marching_cubes) algorithm is used -// to identify an envelope containing the isosurface within the bounding box. The surface -// intersecttion with a voxel cube is then triangulated to form a surface fragment, which is -// combined with all other surface fragments. Ambiguities in triangulating the surfaces -// in certain voxel cube configurations are resolved so that all triangular facets are -// properly oriented with no holes in the surface. If a side of the bounding box clips -// the isosurface, this clipped area is filled in so that the surface remains manifold. -// . -// Be mindful of how you set `voxel_size` and `bounding_box`. For example a voxel size -// of 1 unit with a bounding box volume of 200×200×200 may be noticeably slow, -// requiring calculation and storage of 8,000,000 field values, and more processing -// and memory to generate the triangulated mesh. On the other hand, a voxel size of 5 -// in a 100×100×100 bounding box requires only 8,000 field values and the mesh -// generates fairly quickly, just a handful of seconds. A good rule is to keep the -// number of field values below 10,000 for preview, and adjust the voxel size -// smaller for final rendering. If the isosurface fits completely within the bounding -// box, you can call {{pointlist_bounds()}} on `vnf[0]` returned from the -// `isosurface()` function to get an idea of a more optimal smaller bounding box to use, -// possibly allowing increasing resolution by decresing the voxel size. You can also set -// the parameter `show_stats=true` to get the bounds of the voxels containing the surface. -// . -// 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 (often -// resulting from clipping by the bounding box) 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. -// 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. -// 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. -// --- -// 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 -// 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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); -// 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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); -// 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( -// p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] -// ) 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); - -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); - 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.") - let( - isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, - isovalmax = is_list(isovalue) ? isovalue[1] : INF, - newbbox = let( // new bounding box quantized for voxel_size - hv = 0.5*voxel_size, - 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), - tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, - tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, - trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), - faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], - dummy = show_stats ? _showstats(voxel_size, newbbox, isovalmin, cubes, faces) : 0 -) [trianglepoints, faces]; - - -// Function&Module: isosurface_array() -// Synopsis: Creates a 3D isosurface from a 3D array of densities. -// SynTags: Geom,VNF -// Topics: Isosurfaces, VNF Generators -// Usage: As a module -// isosurface_array(voxel_size, isovalue, fields, [origin=], [reverse=], [close_clip=], [show_stats=]); -// Usage: As a function -// vnf = isosurface_array(voxel_size, isovalue, fields, [origin=], [reverse=], [close_clip=], [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 -// range of isovalues. -// When called as a module, displays the isosurface within the passed array 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. -// . -// Use this when you already have a 3D array of intensity or density data, for example like -// what you may get from a [CT scan](https://en.wikipedia.org/wiki/CT_scan). -// . -// By default, the returned VNF structure occupies a volume with its origin at [0,0,0] -// extending in the positive x, y, and z directions by multiples of `voxel_size`. -// This origin can be overridden by the `origin` parameter. -// . -// 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 -// 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. -// --- -// 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] -// 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 -// 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 = [ -// repeat(0,[6,6]), -// [ [0,1,2,2,1,0], -// [1,2,3,3,2,1], -// [2,3,4,4,3,2], -// [2,3,4,4,3,2], -// [1,2,3,3,2,1], -// [0,1,2,2,1,0] -// ], -// [ [0,0,0,0,0,0], -// [0,0,1,1,0,0], -// [0,2,3,3,2,0], -// [0,2,3,3,2,0], -// [0,0,1,1,0,0], -// [0,0,0,0,0,0] -// ], -// [ [0,0,0,0,0,0], -// [0,0,0,0,0,0], -// [0,1,2,2,1,0], -// [0,1,2,2,1,0], -// [0,0,0,0,0,0], -// [0,0,0,0,0,0] -// ], -// repeat(0,[6,6]) -// ]; -// rotate([0,-90,180]) -// isosurface_array(voxel_size=10, -// isovalue=0.5, fields=fields); - -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); - 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.") - 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 - ) [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), - tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, - tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, - trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), - faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], - dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 -) [trianglepoints, faces]; - - -/// isosurface_cubes() - private function, called by isosurface() -/// This implements a marching cube algorithm, sacrificing some memory in favor of speed. -/// Return a list of voxel cube structures that have one or both surfaces isovalmin or isovalmax intersecting them, and cubes inside the isosurface volume that are at the bounds of the bounding box. -/// The cube structure is: -/// [cubecoord, cubeindex_isomin, cubeindex_isomax, field, bfaces] -/// where -/// cubecoord is the [x,y,z] coordinate of the front left bottom corner of the voxel, -/// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections -/// cf is vector containing the 6 field strength values at each corner of the voxel cube -/// 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( - // get field intensities - fields = 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) - ] - ] - ], - nx = len(fields)-2, - ny = len(fields[0])-2, - nz = len(fields[0][0])-2, - v0 = bbox[0] -) [ - for(i=[0:nx]) let(x=v0[0]+voxsize*i) - for(j=[0:ny]) let(y=v0[1]+voxsize*j) - 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] - ], - mincf = min(cf), - maxcf = max(cf), - cubecoord = [x,y,z], - bfaces = close_clip ? _bbox_faces(cubecoord, voxsize, bbox) : [], - cubefound_isomin = (mincf<=isovalmin && isovalmin isoval ? 1 : 0) + - (f[1] > isoval ? 2 : 0) + - (f[2] > isoval ? 4 : 0) + - (f[3] > isoval ? 8 : 0) + - (f[4] > isoval ? 16 : 0) + - (f[5] > isoval ? 32 : 0) + - (f[6] > isoval ? 64 : 0) + - (f[7] > isoval ? 128 : 0); - - -/// _isosurface_trangles() - called by isosurface() -/// Given a list of voxel cubes structures, triangulate the isosurface(s) that intersect each cube and return a list of triangle vertices. -function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritablemin, tritablemax) = [ - for(cl=cubelist) let( - v = cl[0], - cbidxmin = cl[1], - cbidxmax = cl[2], - f = cl[3], - bbfaces = cl[4], - vcube = [ - v, v+[0,0,cubesize], v+[0,cubesize,0], v+[0,cubesize,cubesize], - v+[cubesize,0,0], v+[cubesize,0,cubesize], - v+[cubesize,cubesize,0], v+[cubesize,cubesize,cubesize] - ], - epathmin = tritablemin[cbidxmin], - epathmax = tritablemax[cbidxmax], - lenmin = len(epathmin), - lenmax = len(epathmax), - outfacevertices = flatten([ - for(bf = bbfaces) - _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) - ]), - n_outer = len(outfacevertices) - ) - // bunch of repeated code here in an attempt to gain some speed to avoid function calls and calls to flatten(). - // Where the face of the bounding box clips a voxel, those are done in separate if() blocks and require require a concat(), but the majority of voxels can have triangles generated directly. If there is no clipping, the list of trianges is generated all at once. - if(lenmin>0 && lenmax>0) let( - // both min and max surfaces intersect a voxel clipped by bounding box - list = concat( - // min surface - [ for(ei=epathmin) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], - // max surface - [ for(ei=epathmax) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(n_outer>0 && lenmin>0) let( - // only min surface intersects a voxel clipped by bounding box - list = concat( - [ for(ei=epathmin) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(lenmin>0) - // only min surface intersects a voxel - for(ei=epathmin) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) - else if(n_outer>0 && lenmax>0) let( - // only max surface intersects the voxel on the bounding box - list = concat( - [ for(ei=epathmax) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(lenmax>0) - // only max surface intersects the voxel - for(ei=epathmax) let( - edge = _MCEdgeVertexIndices[ei], - vi0 = edge[0], - vi1 = edge[1], - denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) - else if(n_outer>0) - // no surface intersects a voxel clipped by bounding box but the bounding box at this voxel is inside the volume between isomin and isomax - for(ls = outfacevertices) ls -]; - - -/// Generate triangles for voxel faces clipped by the bounding box -function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( - vi = _MCFaceVertexIndices[bbface], - vfc = [ for(i=vi) vcube[i] ], - fld = [ for(i=vi) f[i] ], - pgon = flatten([ - for(i=[0:3]) let( - vi0=vi[i], - vi1=vi[(i+1)%4], - f0 = f[vi0], - f1 = f[vi1], - lowhiorder = (f0=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f0)/denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), - if(fbetweenlow && f0>=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f0)/denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) - - ] - ]), - npgon = len(pgon), - triangles = npgon==0 ? [] : [ - for(i=[1:len(pgon)-2]) [pgon[0], pgon[i], pgon[i+1]] - ]) flatten(triangles); - - -/// _showstats() (Private function) - called by isosurface() and isosurface_array() -/// Display statistics about isosurface -function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( - v = column(cubes, 0), // extract cube vertices - x = column(v,0), // extract x values - y = column(v,1), // extract y values - z = column(v,2), // extract z values - xmin = min(x), - xmax = max(x)+voxelsize, - ymin = min(y), - ymax = max(y)+voxelsize, - zmin = min(z), - zmax = max(z)+voxelsize, - ntri = len(faces), - nvox = len(cubes) -) echo(str("\nIsosurface statistics:\n Outer isovalue = ", isoval, "\n Voxel size = ", voxelsize, - "\n Voxels found containing surface = ", nvox, "\n Triangles = ", ntri, - "\n Bounding box for all data = ", bbox, - "\n Bounding box for isosurface = ", [[xmin,ymin,zmin], [xmax,ymax,zmax]], - "\n")) 0; - - -/// ---------- metaball stuff starts here, uses isosurface_array() above ---------- - -/// metaball function literal indices - -MB_SPHERE=0; -MB_ELLIPSOID=1; -MB_ROUNDCUBE=2; -MB_CUBE=3; -MB_OCTAHEDRON=4; -MB_TORUS=5; -MB_CUSTOM=6; - -/// Built-in metaball functions corresponding to each MB_ index. -/// Each function takes three parameters: -/// cdist = cartesian distance, a vector [dx,dy,dz] being the distances from the ball center to the volume sample point -/// charge = the charge of the metaball, can be a vector if the charges are different on each axis. -/// additional (named whatever's convenient) = additional value or array of values needed by the function. -/// rcutoff = radial cutoff; effect suppression increases with distance until zero at the rcutoff distance, and is zero from that point farther out. Default: INF - -_metaball_sphere = function (cdist, charge, unused, rotm, rcutoff) -let( - r = norm(cdist), - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a -) r==0 ? 10000*charge : suppress*charge / r; - -_metaball_ellipsoid = function (cdist, charge, unused, rotm, rcutoff) -let( - dist = concat(cdist,1) * rotm, - r = norm([dist[0]/charge[0], dist[1]/charge[1], dist[2]/charge[2]]), - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a, - sgn = sign(charge[0]*charge[1]*charge[2]) -) r==0 ? 10000*sgn*max(abs(charge[0]), abs(charge[1]), abs(charge[2])) - : suppress*sgn / r; - -_metaball_roundcube = function (cdist, charge, exponent, rotm, rcutoff) -let( - dist = concat(cdist,1) * rotm, - r = abs(dist[0])^exponent + abs(dist[1])^exponent + abs(dist[2])^exponent, - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a -) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge)^exponent / r; - -_metaball_cube = function (cdist, charge, unused, rotm, rcutoff) -let( - dist = concat(cdist,1) * rotm, - r = max(abs(dist[0]), abs(dist[1]), abs(dist[2])), - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a -) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge) / r; - -_metaball_octahedron = function (cdist, charge, unused, rotm, rcutoff) -let( - dist = concat(cdist,1) * rotm, - r = abs(dist[0]) + abs(dist[1]) + abs(dist[2]), - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a -) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge) / r; - -_metaball_torus = function (cdist, charge, axis, rotm, rcutoff) -let( - tmp = concat(cdist,1) * rotm, - dist = [tmp[0], tmp[1], tmp[2]], - bigdia = abs(charge[0]), - smalldia = charge[1], - d_axisplane = norm(v_mul([1,1,1]-axis, dist)) - bigdia, - d_axis = axis*dist, - r = norm([d_axisplane, d_axis]), - suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a -) r==0 ? 1000*max(charge) : suppress*sign(charge[0])*smalldia / r; - - -/// metaball field function, calling any of the other metaball functions above to accumulate -/// the contribution of each metaball at point xyz -_metaball_fieldfunc = function(xyz, nballs, ball_centers, charges, ball_type, rotmatrix, additional, rcutoff, funcs) -let( - contrib = [ - for(i=[0:nballs-1]) let( - dist = xyz-ball_centers[i], - func = ball_type[i]==MB_CUSTOM ? funcs[MB_CUSTOM][i] : funcs[ball_type[i]] - ) func(dist, charges[i], additional[i], rotmatrix[i], rcutoff[i]) - ] -) sum(contrib); - - -// Function&Module: metaballs() -// Synopsis: Creates a model of metaballs within a bounding box. -// SynTags: Geom,VNF -// Topics: Metaballs, Isosurfaces, VNF Generators -// See Also: isosurface_array() -// Usage: As a module -// metaballs(voxel_size, bounding_box, isovalue, ball_centers, [ball_sizes=], [ball_type=], [rotation=], [field_function=], [radial_cutoff=], [close_clip=], [show_stats=]); -// Usage: As a function -// vnf = metaballs(voxel_size, bounding_box, isovalue, ball_centers, [ball_sizes=], [ball_type=], [rotation=], [field_function=], [radial_cutoff=], [close_clip=], [show_stats=]); -// Description: -// [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", -// are organic-looking ball-shaped blobs that meld together when in close proximity. -// The melding property is determined by an interaction formula based on the "charge" of -// each ball and their distance from one another. If you consider a "ball" to be a point -// charge in 3D space, the electric field surrounding that charge decreases in intensity -// with distance from the charge. The metaball is the isosurface corresponding to all value -// where the electric field intensity is a constant value. -// A stronger charge results in a stronger the electric field, and correspondingly a -// larger metaball. Fields from two charges add together, changing the shape of the two -// corresponding metaballs when they are in close proximity. -// . -// In physics, the electric field intensity falls off as an inverse-square relationship -// with distance; that is, the field is proportional to $1/r^2$ where $r$ is the radial -// distance from the point charge. However, most implementations of metaballs instead use -// a simple inverse relationship proportional to $1/r$. That is true for the field -// types available here, or you can define your own falloff function as the -// `field_function` parameter. -// . -// Six shapes of fields around each metaball center are possible. You can specify -// different types for each metaball in the list, and you can also specify your own -// custom field equation. The five types are: -// * `MB_SPHERE` - the standard spherical metaball with a 1/r field strength falloff. -// * `MB_ELLIPSOID` - an ellipsoid-shaped field that requires specifying a [x,y,z] vector for the charge, representing field strength in each of the x, y, and z directions -// * `MB_ROUNDCUBE` - a cube-shaped metaball with corners that get more rounded with size. The squareness can be controlled with a value between 0 (spherical) or 1 (cubical) in the `additional` parameter, and defaults to 0.5 if omitted. -// * `MB_CUBE` - a cube-shaped metaball with sharp edges and corners, resulting from using [Chebyshev distance](https://en.wikipedia.org/wiki/Chebyshev_distance) rather than Euclidean distance calculations. -// * `MB_OCTAHEDRON` - an octahedron-shaped metaball with sharp edges and corners, resulting from using [taxicab distance](https://en.wikipedia.org/wiki/Taxicab_geometry) rather than Euclidean distance calculations. -// * `MB_TORUS` - a toroidal field oriented perpendicular to the x, y, or z axis. The `charge` is a two-element vector determining the major and minor diameters, and the `additional` paramater sets the axis directions for each ball center (defaults to [0,0,1] if not set). -// * `MB_CUSTOM` - your own custom field definition, requiring you to set the `field_function` parameter to your own function literal. -// If either `MB_ELLIPSOID` or `MB_TORUS` occur in the list, the list of charges **must** be explicitly defined rather than supplying a single value for all. -// Arguments: -// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the metaball surface. -// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The voxels needn't fit perfectly inside the bounding box. -// isovalue = A scalar value specifying the isosurface value of the metaballs. -// ball_centers = an array of 3D points specifying each of the metaball centers. -// --- -// charge = a single value, or an array of values corresponding to `ball_centers`, specifying the charge intensity of each ball center. Default: 10 -// ball_type = shape of field that falls off from the metaball center. Can be one of `MB_SPHERE`, `MB_ELLIPSOID`, `MB_ROUNDCUBE`, `MB_CUBE`, `MB_OCTAHEDRON`, `MB_TORUS`, or `MB_CUSTOM`. This may be an array of values corresponding to each ball. Where this value is `MB_CUSTOM`, the corresponding array element in `field_function` must also be set. Default: `_MB_SPHERE` -// rotation = A vector `[x_rotation, y_rotation, z_rotation]`, or list of vectors for each ball, specifying the rotation angle in degrees around the x, y, and z axes. This is meaningless for `_MB_SPHERE` but allows you to orient the other metaball types. Default: undef -// field_function = A single [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) or array of function literals that return a single field value from one metaball, and takes as inputs a 3D distance vector, a single charge or list of charges, and a single additional parameter or list of parameters (that third parameter must exist in the function definition even if it isn't used). If the corresponding `ball_type` parameter is not `MB_CUSTOM`, then the function specified in `ball_type` is used instead; only where `ball_type` is `MB_CUSTOM` does this custom field function get invoked. Default: undef -// additional = A single value, or a list of optional additional parameters that may be required by the field function. If you make a custom function, it is your responsibility to create a function literal compatible with these inputs. Nothing is passed to the function literal if `additional` is not set. This parameter must be specified as an entire list for all metaballs if MB_ELLIPSOID or MB_TORUS is included in `ball_type`. Default: `undef` for `ball_type=CUSTOM` -// radial_cutoff = Maximum radial distance of a metaball's influence. This isn't a sharp cutoff; rather, the suppression increases with distance until the influence is zero at the `radial_cutoff` distance. Can be a single value or an array of values corresponding to each ball center, but typically it's sufficient to supply a single value approximately the average separation of each ball, so each ball mostly acts on its nearest neighbors. Default: INF -// 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 -// 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; -// 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); -// 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; -// 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); -// 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]; -// 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); -// 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]]; -// 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); -// 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) -// let( -// r = norm(cdist) + rands(0, 0.2, 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]; -// voxelsize = 0.4; -// boundingbox = [[-16,-8,-8], [16,8,8]]; -// -// metaballs(voxelsize, boundingbox, isovalue=1, -// ball_centers=centers, charge=charge, ball_type=type, -// field_function=fieldfuncs); -// 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 -// ]; -// 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_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); - 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.") -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 - ], - - // set up field array - v0 = bounding_box[0], - b1 = bounding_box[1], - halfvox = 0.5*voxel_size, - fieldarray = [ - 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) - ] - ] - ] -) 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 -]; -======= -======= ->>>>>>> Stashed changes ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), @@ -2897,7 +1479,3 @@ function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 ) [trianglepoints, faces]; -<<<<<<< Updated upstream ->>>>>>> Stashed changes -======= ->>>>>>> Stashed changes From 8bf95194f8d851338f8195e27733ff65e7101eaf Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 5 Feb 2025 16:19:08 -0800 Subject: [PATCH 05/11] Update isosurface.scad --- isosurface.scad | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index 148b5a7b..c64d1a99 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1444,11 +1444,11 @@ function metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, sho // repeat(0,[6,6]) // ]; // rotate([0,-90,180]) -// isosurface_array(field, isovalue=0.5, +// isosurface(field, isovalue=0.5, // voxel_size=10); module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _origin=undef) { - vnf = isosurface(f, isovalue, bounding_box, voxel_size, reverse, closed, show_stats, _origin); + vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, show_stats, _origin); vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children(); } @@ -1472,7 +1472,7 @@ function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed nz = len(f[0][0])-1 ) is_def(_origin) ? [_origin, _origin+voxel_size*[nx,ny,nz]] : [-0.5*voxel_size*[nx,ny,nz], 0.5*voxel_size*[nx, ny, nz]], - cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=f, isovalmin=isovalmin, isovalmax=isovalmax, closed=closed), + cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, 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), From 65d5471441d686134cac9ab713ab244fc9facae4 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Sun, 9 Feb 2025 13:38:10 -0800 Subject: [PATCH 06/11] Update isosurface.scad --- isosurface.scad | 632 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 431 insertions(+), 201 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index c64d1a99..b365185b 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,33 +1,39 @@ ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), -// are bounded and closed organic-looking surfaces that smoothly meld together when in close proximity. -// Metaballs are a specific example of isosurfaces. +// are bounded and closed organic surfaces that smoothly blend together. +// Metaballs are one specific kind of isosurface. // . -// An isosurface is a three-dimensional surface representing points of a constant -// value (e.g. pressure, temperature, electric potential, density) in a +// An isosurface, or implicit surface, is a three-dimensional surface representing all points of a +// constant 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. +// isosurface **is** a 2D contour. // . -// For computer-aided design, isosurfaces of abstract functions can generate complex -// curved surfaces and organic-looking shapes. Metaballs, for example, are typically generated by -// point sources (the metaball centers) that contribute values to all the points in a 3D volume depending -// on distance to the metaball centers. The combined contributions from all metaballs in the volume -// result variying values throughout the volume. Surfaces that connect all points of equal value in the -// volume that appear as blobby shapes that blend together. +// For computer-aided design, isosurfaces of abstract functions can generate complex curved surfaces +// and organic shapes. For example, spherical metaballs can be formulated using a set of point +// centers that define the metaballs locations. For a single metaball, a function is defined each +// each point in a 3D volume based on the distance from that point to the metaball center. The +// combined contributions from all the metaballs results in a function that varies in a complicated +// way across the volume. When two metaballs are far apart, they appear simply as spheres, but when +// they are close together they enlarge amd reach toward each other and meld together in a smooth +// fashion. The resulting metaball model appears as smoothly blended blobby shapes. The +// implementation below provides metaballs of a variety of types including spheres, cuboids and +// cylinders (cones), with optional parameters to adjust the influence of one metaball on others, +// and the cutoff distance where the metaball's influence stops. // . -// In general temrs, an isosurface may be represented by any function of three variables, -// that is, the isosurface of a function $f(x,y,z)$ is the set of points where +// An isosurface can be defined using any function of three variables: +// 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". -// We use the term "isovalue" in the context of metaballs also, although you may find other applications -// referring to "threshold", which means the same thing. +// Changing the isovalue will tend to grow or shrink the isosurface, depending on how the function is +// defined. Since metaballs are isosurfaces, they also have an isovalue. The isovalue is also known +// as the "threshold". // . // Some isosurface functions are unbounded, extending infinitely in all directions. A familiar example may // be a [gryoid](https://en.wikipedia.org/wiki/Gyroid), which is often used as a volume infill pattern in // [fused filament fabrication](https://en.wikipedia.org/wiki/Fused_filament_fabrication)). The gyroid // isosurface is unbounded and periodic in all three dimensions. // . -// Below are modules and functions to create manifold 3D models of metaballs and other isosurfaces. +// This file provides modules and functions to create a VNF using metaballs, or from general isosurfaces. // // Includes: // include @@ -682,9 +688,9 @@ function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isov 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]]) + for(x=[v.x:voxsize:b1.x]) [ + for(y=[v.y:voxsize:b1.y]) [ + for(z=[v.z:voxsize:b1.z]) fieldfunc(x,y,z) ] ] @@ -765,7 +771,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], // max surface [ for(ei=epathmax) let( @@ -773,7 +779,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) ) for(ls = list) ls else if(n_outer>0 && lenmin>0) let( @@ -785,7 +791,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) ) for(ls = list) ls else if(lenmin>0) @@ -796,7 +802,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) else if(n_outer>0 && lenmax>0) let( @@ -807,7 +813,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) ) for(ls = list) ls else if(lenmax>0) @@ -818,7 +824,7 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) else if(n_outer>0) @@ -843,7 +849,7 @@ function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( fmax = max(f0, f1), fbetweenlow = (fmin <= isovalmin && isovalmin <= fmax), fbetweenhigh = (fmin <= isovalmax && isovalmax <= fmax), - denom = f1 - f0 + denom = f1==INF || f0==INF ? 0 : f1-f0 ) [ if(isovalmin <= f0 && f0 <= isovalmax) vcube[vi0], if(fbetweenlow && f0<=f1) let( @@ -935,7 +941,7 @@ function _mb_sphere_full(dv, r, ex, cutoff, neg) = let(dist=norm(dv)) function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") - assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") let( r = get_radius(r=r,d=d), dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), @@ -946,71 +952,150 @@ function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = : influence==1 ? function(dv) _mb_sphere_cutoff(dv,r,cutoff,neg) : function(dv) _mb_sphere_full(dv,r,1/influence,cutoff,neg); -// metaball cylinder +// metaball capsule (round-ended cylinder) -function _mb_cylinder_basic(dv, r, hl, neg) = let( +function _mb_capsule_basic(dv, hl, r, neg) = let( dist = dv.z<-hl ? norm(dv-[0,0,-hl]) - : dv.z0, "\ncutoff must be a positive number.") - assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") - assert(is_num(length) && length>0, "\nlength must be a positive number.") - let( - r = get_radius(r=r,d=d), - dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), - neg = negative ? -1 : 1 +function mb_capsule(h, r, cutoff=INF, influence=1, negative=false, d,l,height,length) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + r = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + sh = h-2*r, // straight side length + dum3 = assert(sh>0, "\nTotal length must accommodate rounded ends of cylinder."), + neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_cylinder_basic(dv,r,length/2,neg) - : !is_finite(cutoff) ? function(dv) _mb_cylinder_influence(dv,r,length/2,1/influence, neg) - : influence==1 ? function(dv)_mb_cylinder_cutoff(dv,r,length/2,cutoff,neg) - : function (dv) _mb_cylinder_full(dv, r, length/2, 1/influence, cutoff, neg); + !is_finite(cutoff) && influence==1 ? function(dv) _mb_capsule_basic(dv,sh/2,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_capsule_influence(dv,sh/2,r,1/influence, neg) + : influence==1 ? function(dv) _mb_capsule_cutoff(dv,sh/2,r,cutoff,neg) + : function (dv) _mb_capsule_full(dv, sh/2, r, 1/influence, cutoff, neg); + +// metaball connector cylinder - calls mb_capsule* functions after transform + +function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + dum1 = assert(is_matrix([p1],1,3), "\nConnector start point p1 must be a 3D coordinate."), + dum2 = assert(is_matrix([p2],1,3), "\nConnector end point p2 must be a 3D coordinate."), + r = get_radius(r=r,d=d), + dum3 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1, + dc = p2-p1, // center-to-center distance + midpt = reverse(-0.5*(p1+p2)), + h = norm(dc)/2, // center-to-center length (cylinder height) + transform = submatrix(down(h)*rot(from=dc,to=UP)*move(-p1) ,[0:2], [0:3]) + ) + !is_finite(cutoff) && influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_basic(newdv,h,r,neg) + : !is_finite(cutoff) ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_influence(newdv,h,r,1/influence, neg) + : influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_cutoff(newdv,h,r,cutoff,neg) + : function (dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_full(newdv, h, r, 1/influence, cutoff, neg); + +// metaball rounded cylinder / cone + +function revsurf(dv, path, coef, exp, cutoff) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + mb_cutoff(dist, cutoff) * (inside_check==[] + ? (coef*(1+dist))^exp : (coef/(1+dist))^exp ); + +function revsurf_rounded(path, rounding, cutoff, influence) = + let(shifted = offset(path, delta=-rounding, closed=false)) + function (dv) revsurf(dv, shifted, 1+rounding, 1/influence, cutoff); + +function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influence=1, negative=false) = + let( + r1 = get_radius(r1=r1,r=r, d1=d1, d=d), + r2 = get_radius(r1=r2,r=r, d1=d2, d=d), + h = first_defined([h,l,height,length],"h,l,height,length") + ) + assert(is_finite(rounding) && rounding>=0, "rounding must be a nonnegative number") + assert(is_finite(r1) && r1>0, "r/r1/d/d1 must be a positive number") + assert(is_finite(r2) && r2>0, "r/r2/d/d2 must be a positive number") + let( + vang = atan2(r1-r2,h), + facelen = adj_ang_to_hyp(h, abs(vang)), + roundlen1 = rounding/tan(45-vang/2), + roundlen2 = rounding/tan(45+vang/2), + sides = [[0,h/2],[r2,h/2],[r1,-h/2], [0, -h/2]], + ) + assert(roundlen1 <= r1, "size of rounding is larger than the r1 radius of the cylinder/cone") + assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") + assert(roundlen1+roundlen2 <=facelen, "Roundings don't fit on the edge length of the cylinder/cone") + revsurf_rounded(sides, rounding, influence); + // metaball rounded cube -function _mb_roundcube_basic(dv, siz, xp, neg) = let( +function _mb_cuboid_basic(dv, siz, xp, neg) = let( dist = xp >= 1100 ? max(v_abs(dv)) : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) ) neg*siz/dist; -function _mb_roundcube_influence(dv, siz, xp, ex, neg) = let( +function _mb_cuboid_influence(dv, siz, xp, ex, neg) = let( dist = xp >= 1100 ? max(v_abs(dv)) :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) ) neg * (siz/dist)^ex; -function _mb_roundcube_cutoff(dv, siz, xp, cutoff, neg) = let( +function _mb_cuboid_cutoff(dv, siz, xp, cutoff, neg) = let( dist = xp >= 1100 ? max(v_abs(dv)) : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) ) neg * mb_cutoff(dist, cutoff) * siz/dist; -function _mb_roundcube_full(dv, siz, xp, ex, cutoff, neg) = let( +function _mb_cuboid_full(dv, siz, xp, ex, cutoff, neg) = let( dist = xp >= 1100 ? max(v_abs(dv)) :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) ) neg * mb_cutoff(dist, cutoff) * (siz/dist)^ex; -function mb_roundcube(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = +function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") - assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") - assert(is_num(size) && size>0, "\nsize must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_finite(size) && size>0, "\nsize must be a positive number.") let( dummy=assert(is_num(size) && size>0, "\ninvalid size."), xp = _squircle_se_exponent(squareness), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_roundcube_basic(dv, size/2, xp, neg) -: !is_finite(cutoff) ? function(dv) _mb_roundcube_influence(dv, size/2, xp, 1/influence, neg) -: influence==1 ? function(dv) _mb_roundcube_cutoff(dv, size/2, xp, cutoff, neg) -: function (dv) _mb_roundcube_full(dv, size/2, xp, 1/influence, cutoff, neg); + !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, size/2, xp, neg) + : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, size/2, xp, 1/influence, neg) + : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, size/2, xp, cutoff, neg) + : function (dv) _mb_cuboid_full(dv, size/2, xp, 1/influence, cutoff, neg); // metaball octahedron @@ -1025,7 +1110,7 @@ function _mb_octahedron_full(dv, r, ex, cutoff, neg) = function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") - assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_finite(influence) && is_num(influence) && influence>0, "\ninfluence must be a positive number.") let( r = get_radius(r=r,d=d), dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), @@ -1049,14 +1134,23 @@ function _mb_torus_full(dv, rmaj, rmin, ex, cutoff, neg) = let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; -function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min) = +function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min, or,od,ir,id) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") - assert(is_num(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") let( - r_maj = get_radius(r=r_maj,d=d_maj), - r_min = get_radius(r=r_min,d=d_min), - dum1=assert(is_finite(r_maj) && r_maj>0, "\ninvalid major radius or diameter."), - dum2=assert(is_finite(r_min) && r_min>0, "\ninvalid minor radius or diameter."), + _or = get_radius(r=or, d=od, dflt=undef), + _ir = get_radius(r=ir, d=id, dflt=undef), + _r_maj = get_radius(r=r_maj, d=d_maj, dflt=undef), + _r_min = get_radius(r=r_min, d=d_min, dflt=undef), + r_maj = is_finite(_r_maj)? _r_maj : + is_finite(_ir) && is_finite(_or)? (_or + _ir)/2 : + is_finite(_ir) && is_finite(_r_min)? (_ir + _r_min) : + is_finite(_or) && is_finite(_r_min)? (_or - _r_min) : + assert(false, "Bad major size parameter."), + r_min = is_finite(_r_min)? _r_min : + is_finite(_ir)? (maj_rad - _ir) : + is_finite(_or)? (_or - maj_rad) : + assert(false, "\nBad minor size parameter."), neg = negative ? -1 : 1 ) !is_finite(cutoff) && influence==1 ? function(dv) _mb_torus_basic(dv, r_maj, r_min, neg) @@ -1067,18 +1161,18 @@ function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, /* hard-edge cylinder -_mb_cylinder = function (dv, coeff, length, cutoff, influence) +_mb_capsule = function (dv, coeff, length, cutoff, influence) let( dist = max(abs(dv.z*2*coeff/length), norm([dv.x,dv.y])), suppress = let(a = min(r,cutoff)/cutoff) 1-a*a, ) suppress*coeff / dist; -function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (dv) _mb_cylinder(dv, coeff, length, cutoff, influence); +function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv) _mb_capsule(dv, coeff, length, cutoff, influence); */ // Function&Module: metaballs() -// Synopsis: Creates a model of metaballs within a bounding box. +// Synopsis: Creates a group of 3D metaballs (smoothly connected blobs). // SynTags: Geom,VNF // Topics: Metaballs, Isosurfaces, VNF Generators // See Also: isosurface_array() @@ -1087,80 +1181,104 @@ function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (d // Usage: As a function // vnf = metaballs(funcs, bounding_box, voxel_size, [isovalue=], [closed=], [show_stats=]); // Description: +// ![Metaball andimation](./images/metaball_demo.png) +// . // [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", -// are organic-looking ball-shaped blobs that meld together when in close proximity. -// The melding property is determined by an interaction formula based on the coefficient -// weight (which can be thought of as a charge, strength, density, or intensity) of -// each ball and their distances from one another. +// can produce soothly varying blobs and organic forms. You create metaballs by placing metaball +// objects at different locations. These objects have a basic size and shape when placed in +// isolation, but if another metaball object is nearby, the two objects interact, growing larger +// and melding together. The closer the objects are, the more they blend and meld. // . -// One analagous way to think of metaballs is, consider each "ball" to be a point-light source in -// a dark room. Pick an illumination value, and every point in the volume of the room with -// that intensity of illumination defines the isosurface, which would be a sphere around a -// single source, or a blob surrounding multiple points because the illumination is additive between them. +// The simplest metaball specification is a list of alternating transformation matrices and +// metaball functions: `[trans0, func0, trans1, func1, ... ]`. The transformation matrix +// you supply can be constructed using the usual transformation commands such as {{up()}}, +// {{right()}}, {{back()}}, {{move()}}, {{scale()}}, {{rot()}} and so on. You can multiply +// the transformations together, similar to how the transformations can be applied +// to regular objects in OpenSCAD. For exmaple, to transform an object in regular OpenSCAD you might +// write `up(5) xrot(25) zrot(45) scale(4)`. You would provide that transformation +// as the transformation matrix `up(5) * xrot(25) * zrot(45) * scale(4)`. You can use +// scaling to produce an ellipse from a sphere, and you can even use {{skew()}} if desired. +// When no transformation is needed, give `IDENT` as the transformation. // . -// Regardless of how you think of it (charge, light, heat, pressure), a stronger metaball -// intensity results in stronger "field" values around the metaball, and correspondingly a -// larger metaball due to the isosurface of a particular value being farther away. -// A metaball is basically a contour surface; that is, a 3D version of a 2D contour. +// You can create metaballs in a variety of standard shapes using the predefined functions +// listed below. If you wish, you can also create custom metaball shapes using your own functions +// (see Example 11). Three parameters are available on all of the built-in metaballs to control the +// interaction of the metaballs with each other: `cutoff`, `influence`, and `negative'. // . -// Most implementations of metaballs instead use a simple inverse relationship proportional to $1/d$ to -// control how the contributions from each metaball fall off with distance. That -// is the default falloff used for the field types available here. The optional `influence` -// argument is a reciprocal exponent on $d$, defaulting to 1. It controls how much the metaball influences -// others at a given distance. If you set `influence=0.5`, the reciprocal is 2, so you get a $1/d^2$ falloff. +// The `cutoff` parameter specifies the distance beyond which the metaball has no interaction with +// other balls. When you apply `cutoff`, a smooth suppression factor begins begins decreasing the +// interaction strength at half the cutoff distance and reduces the interaction to zero at the cutoff. +// . +// The `influence` parameter adjusts the strength of the interaction metaball objects have with each +// other. If you increase `influence` from its default of 1, the metaball interacts with other +// metaballs at a longer range, and surrounding balls grow bigger. The metaball with larger +// influence can also grow bigger because it couples more strongly with other nearby balls, but it +// can also remain nearly unchanged while influencing others when `isovalue` is greater than 1. +// Decreasing influence has the reverse effect. Small changes in influence can have a large +// effect. For example, setting `influence=2` dramatically increases the interactions at longer +// distances, and you may want to set the `cutoff` argument to limit the range influence. // . -// You can also define your own metaball functions as shown in example 5. +// The `negative` parameter, if set to `true`, creates a negative metaball, which can create +// hollows or dents in other metaballs, or swallow other metaballs entirely, making them disappear. +// Negative metaballs are always below the isovalue, so they are never directly visible; +// only their effects are visible. See Example 7. +// . +// For complicated metaball assemblies you may wish to repeat a structure in different locations or +// otherwise transformed. Nesting metaball specifications are supported: +// Instad of specifying a transform and function, you specify a transform and then a metaball +// specification. For example, you could set `finger=[t0,f0,t1,f1,t2,f2]` and then set +// `hand=[u0,finger,u1,finger,...]` and then invoke metaball with `[s0, hand]`. +// Basically, any list of metaballs is, itself, a metaball. It can be used in place of a function in another list. +// This is a powerful technique that lets you make groups of metaballs that you can use as individual +// metaballs in other groups, and can make your code compact and simpler to understand. See Example 13. // . // .h3 Built-in metaball functions -// Various shapes of metaball field density functions are built into this library. You can specify different -// ones for each metaball in the list, and you can also specify your own custom function. +// Several metaballs are defined for you to use in your models. +// All of the built-in metaballs take positional and named parameters that specify the size of the +// metaball (e.g. radius, height). The size arguments are the same as those for the regular objects +// of the same type (e.g. a sphere accepts both `r` for radius and the named parameter `d=` for +// diameter). The size parameters always specify the size of the metaball **in isolation** with +// `isovalue=1`. The metaballs can grow much bigger than their specified sizes when they interact +// with each other. The metaballs also grow bigger than their specified sizes, even in isolation, +// if `isovalue<1` and smaller than their specified sizes if `isovalue>1`. // . -// All built-in functions have these arguments in common: -// * The first argument is always a size (such as a radius or diameter) that determines the size of -// the metaball. Other size arguments may follow as appropriate. All the -// metaball functions are designed so that an isolated metaball with `isovalue=1` appears with a -// radius or size approximately equal to this coefficient, but the metaball can get -// significantly larger when other metaballs are in the bounding box, depending on proximity. -// * `cutoff` - specifies the distance beyond which the metaball has no influence. -// A smooth suppression factor is applied to the metaball's influence on others, starting at half -// the cutoff distance, suppressing the influence to zero at the cutoff distance. Default: INF -// * `influence` - determines the extent of influence of the metaball. This is an inverse -// distance relationship proportional to $1/d$ where $d$ is distance. The `influence` argument is the -// reciprocal of the exponent; for example, If `influence=0.5`, you get an inverse-square falloff -// $1/d^2$, resulting in less influence at a given distance than the default `influence=1`. Setting -// `influence=2` results in a gentle $1/\sqrt d$ falloff, dramatically increasing -// the influence at distances, and you may want to set the `cutoff` argument to limit that influence. -// * `negative` - when true, causes the metaball to have a negative influence on its surroundings. A -// negative metaball can create hollows or dents in other metaballs, or swallow other metaballs -// entirely, making them disappear if the metaball's negative influence is large. A negative -// metaball is never visible directly, only its effect is visible, because the isosurface surrounds -// only field values greater than the isovalue (see Example 2 below). Default: false +// All of the built-in functions all accept these named arguments, which are not repeated in the list below: +// * `cutoff` - specifies the distance beyond which the metaball has no influence. Default: INF +// * `influence` - a positive number that determines how much influence the metaball has on others. Default: 1 +// * `negative` - when true, causes the metaball to have a negative influence on its surroundings. Default: false // . -// These are the built-in metaball functions. Arguments with default values are optional: -// * `mb_sphere(r/d, cutoff=INF, influence=1, negative=false)` - the standard spherical metaball with a $1/d$ falloff when `influence=1`. The `r` or `d` argument controls the radius or diameter, respectively. For a spherical metaball by itself, you get a sphere of radius `r` at `isovalue=1`. You can create an ellipsoid using `scale()` as the last transformation element in the metaball `funcs` array. -// * `mb_cylinder(r/d, length, cutoff=INF, influence=1, negative=false)` - a cylindrical-shaped field with rounded ends of radius `r` or diameter `d`, useful as a connector. For a single cylindrical metaball by itself at `isovalue=1`, you get a cylinder of radius `r` and straight-side length of `length`, but it grows when other metaballs are nearby. -// * `mb_roundcube(size, squareness=0.5, cutoff=INF, influence=1, negative=false)` - a cuboid metaball with rounded corners that get more rounded at farther distances, depending on isovalue and influence from other metaballs. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5 if omitted. By itself with `isovalue=1`, you get a rounded cube having `size` distance from side to side. -// * `mb_octahedron(r/d, cutoff=INF, influence=1, negative=false)` - an octahedron-shaped metaball with sharp edges and corners, resulting from using [taxicab distance](https://en.wikipedia.org/wiki/Taxicab_geometry) rather than Euclidean distance calculations. By itself with `isovalue=1` you get an octahedron with tip radius of `r` or tip-to-top diameter `d`. -// * `mb_torus(r_maj/d_maj, r_min/d_min, cutoff=INF, influence=1, negative=false)` - a toroidal field oriented perpendicular to the z axis. The arguments `r_maj` and `r_min` control the major and minor radii; otherwise `d_maj` and `d_min` control the major and minor diameters. +// The built-in metaball functions are: +// * `mb_sphere(r|d=)` - spherical metaball, with radius r or diameter d. You can create an ellipsoid using `scale()` as the last transformation entry of the metaball `funcs` array. +// * `mb_cuboid(size, [squareness=0.5])` - cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` specifies the dimensions of the cuboid shape between the face centers, but except when `squareness=1`, the faces are always a little bit curved. +// * `mb_cyl(h|l|height|length=, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=0])` - vertical cylinder or cone metaball with the same dimenional arguments as {{cyl()}}. The rounding is the same at both ends. If you just want a cylindrical shape, consider `mb_capsule()`, which has a faster execution time. +// * `mb_capsule(h|l|height|length=, r|d=)` - vertical cylinder of radius `r` or diameter `d` with hemispherical caps. The height or length specifies the **total** height including the rounded ends. +// * `mb_connector(p1, p2, r|d=)` - cylinder of radius `r` or diameter `d` with hemispherical caps (like mb_capsule), but specified to connect point `p1` to point `p2` (where `p1` and `p2` are 3D vectors). The specified points are at the ends of the straight portion of the shape (the centers of the two capping hemispheres). +// * `mb_octahedron(r|d=])` - octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip. The vertex parameter specifies the distance between the two opposite tips. +// * `mb_torus([r_maj|d_maj=], [r_min|d_min=],[or=|od=],[ir=|id=])` - torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. // . -// Your own custom function must be written as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) -// and take `dv` as the first argument with a size as the second argument. `dv` is passed to your -// function as a 3D distance vector from the ball center to the point in the bounding box volume for -// which to calculate the field intensity. The function must return a single number such that higher -// values are enclosed by the metaball and lower values are outside the metaball. -// In this case, if you have written `my_func()`, the array element you initialize must appear -// as `function (dv) my_func(dv, ...)`. See Example 5 below. +// .h3 Metaball functions and user defined functions +// Each metaball function is defined as a function of a 3-vector that gives the value of the metaball function +// for that point in space. As is common in metaball implementations, we define the built-in metaballs using an +// inverse relationship where the metaball functions fall off as $1/d$. The spherical metaball therefore has +// a simple basic definition as `f(v) = 1/norm(v)`. Note that with this framework, `f(v)>=c` defines a bounded +// object. Increasing the isovalue shrinks the object, and decreasing the isovalue grows the object. // . -// Now for the arguments to this metaball() module or function.... +// In order to adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` then the +// decay becomes $1/d^(1/a)$. This means, for example, that if you set influence to 2 you get a $1/d^2$ falloff. +// Changing this exponent changes how the balls interact. +// . +// You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// that takes a single argument (a 3-vector) and returns a single numerical value. +// The returned value should define a function where in isovalue range [c,INF] defines a bounded object. See Example 11. // Arguments: -// funcs = a 1-D list of transform and function pairs in the form `[trans0, func0, trans1, func1, ...]`, with one pair for each metaball. The transform should be at least a position such as `move([x,y,z])` to specify the location of the metaball center, but you can also include rotations, such as `move([x,y,z])*rot([ax,ay,az])`. You can multiply together any of BOSL2's affine operations like {{xrot()}}, {{scale()}}, and {{skew()}}. This is useful for orienting non-spherical metaballs. The priority order of the transforms is right to left, that is, `move([4,5,6])*rot([45,0,90])` does the rotation first, and then the move, similar to normal OpenSCAD syntax `translate([4,5,6]) rotate([45,0,90]) children()`. +// funcs = a 1-D list of transform and function pairs in the form `[trans0, func0, trans1, func1, ...]`, with one pair for each metaball. The transform should be at least a position such as `move([x,y,z])` to specify the location of the metaball center, but you can also include rotations, such as `move([x,y,z])*rot([ax,ay,az])`. You can multiply together any of BOSL2's affine operations like {{xrot()}}, {{scale()}}, and {{skew()}}. This is useful for orienting non-spherical metaballs. The priority order of the transforms is right to left, that is, `move([4,5,6])*rot([45,0,90])` does the rotation first, and then the move, similar to normal OpenSCAD syntax `translate([4,5,6]) rotate([45,0,90]) children()`. The transform `IDENT` may be used if you don't want to specify a transform, resulting in a metaball positioned at the origin. **Any function in the list may, itself, be another list of metaballs instead of a function name.** See Example 13 for a demonstration. // voxel_size = The size (scalar) of the voxel cube that determines the resolution of the metaball surface. **Start with a larger size for experimenting, and refine it gradually.** A small voxel size can significantly slow down processing time, especially with a large `bounding_box`. // bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The voxels needn't fit perfectly inside the bounding box. -// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the coefficient corresponds to the radial size of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 +// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 // --- // 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 -// convexity = Max number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// 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 small speed penalty. Default: false +// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 // cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" // anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` // spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` @@ -1171,11 +1289,58 @@ function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (d // "intersect" = Anchors to the surface of the shape. // Named Anchors: // "origin" = Anchor at the origin, oriented UP. +// Example(3D,VPD=110): This is the first of a series of five examples demonstrating the differnent types of metaball interactions. We start with two spheres 30 units apart. Each would have a radius of 10 in isolation, but because they are influencing their surroundings, each sphere mutually contributes to the size of the other. The sum of contributions between the spheres add up so that a surface plotted around the region exceeding the threshold defined by `isovalue=1` looks like a peanut shape surrounding the two spheres. +// funcs = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(funcs, voxelsize, boundingbox, +// isovalue=1); +// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (which is the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself. +// funcs = [ +// left(15), mb_sphere(10, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(funcs, voxelsize, boundingbox, +// isovalue=1); +// Example(3D,VPD=110): Here, the left sphere has less influence in addition to a cutoff. Setting `influence=0.5` results in a steeper falloff of contribution from the left sphere. Each sphere has a different size and shape due to unequal contributions based on distance. +// funcs = [ +// left(15), mb_sphere(10, influence=0.5, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(funcs, voxelsize, boundingbox, +// isovalue=1); +// Example(3D,VPD=110): In this example, we have two size-10 spheres as before and one tiny sphere of 1.5 units radius offset a bit on the y axis. With an isovalue of 1, this figure would appear similar to Example 1 above, but here the isovalue has been set to 2, causing the surface to shrink around a smaller volume values greater than 2. Remember, higher isovalue thresholds cause metaballs to shrink. +// funcs = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10), +// fwd(15), mb_sphere(1.5) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(funcs, voxelsize, boundingbox, +// isovalue=2); +// Example(3D,VPD=110): The influence of the tiny sphere has been set quite high, to 10. Notice that the tiny sphere does not change in size, but its contribution to its surroundings has dramatically increased, causing the two other spheres to grow and meld into each other. The `influence` argument on a small metaball affects its surroundings more than itself. +// funcs = [ +// move([-15,0,0]), mb_sphere(10), +// move([15,0,0]), mb_sphere(10), +// move([0,-15,0]), mb_sphere(1.5, influence=10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(funcs, voxelsize, boundingbox, +// isovalue=2); // Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. // 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), +// IDENT, mb_sphere(3), // move([0,0,20]), mb_sphere(5), // move([20,20,10]), mb_sphere(7) // ]; @@ -1196,8 +1361,8 @@ function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (d // color("green") move_copies(centers) sphere(d=1, $fn=16); // Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. // funcs = [ -// move([-7,-3,27])*zrot(55), mb_roundcube(6, squareness=1), -// move([5,5,21]), mb_roundcube(5), +// move([-7,-3,27])*zrot(55), mb_cuboid(6, squareness=1), +// move([5,5,21]), mb_cuboid(5), // move([10,0,10]), mb_octahedron(5) // ]; // voxelsize = 0.5; // a bit slow at this resolution @@ -1230,36 +1395,55 @@ function mb_cylinder(coeff=10, length=10, cutoff=INF, influence=1) = function (d // ) mb_cutoff(dist,cutoff) * (r/dist)^(1/influence); // // funcs = [ -// move([-9,0,0]), mb_sphere(5), -// move([9,0,0]), function (dv) noisy_sphere(dv, 5, 0.2), +// left(9), mb_sphere(5), +// right(9), function (dv) noisy_sphere(dv, 5, 0.2), // ]; // voxelsize = 0.4; // boundingbox = [[-16,-8,-8], [16,8,8]]; // metaballs(funcs, voxelsize, boundingbox, isovalue=1); -// 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. +// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A complex example using ellipsoids, a cylinder, 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. // include // tetpts = zrot(15, p = 22 * regular_polyhedron_info("vertices", "tetrahedron")); -// tetxform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; +// tettransform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; // // funcs = [ // // vertical cylinder arm -// move([0,0,15]), mb_cylinder(2, 17, influence=0.8), +// up(15), mb_capsule(17, 2, influence=0.8), // // ellipsoid arms -// tetxform[0], mb_sphere(1, cutoff=30), -// tetxform[1], mb_sphere(1, cutoff=30), -// tetxform[2], mb_sphere(1, cutoff=30), +// for(i=[0:2]) each [tettransform[i], mb_sphere(1, cutoff=30)], // // ring on top -// move([0,0,35])*xrot(90), mb_torus(r_maj=8, r_min=2.5, cutoff=35), +// up(35)*xrot(90), mb_torus(r_maj=8, r_min=2.5, cutoff=35), // // feet -// move(2.2*tetpts[0]), mb_sphere(5, cutoff=30), -// move(2.2*tetpts[1]), mb_sphere(5, cutoff=30), -// move(2.2*tetpts[2]), mb_sphere(5, cutoff=30) +// for(i=[0:2]) each [move(2.2*tetpts[i]), mb_sphere(5, cutoff=30)], // ]; // voxelsize = 1; // boundingbox = [[-22,-32,-13], [36,32,46]]; // // useful to save as VNF for copies and manipulations // vnf = metaballs(funcs, voxelsize, boundingbox, isovalue=1); // vnf_polyhedron(vnf); +// Example(3D,Med,NoAxes): Example of grouping metaballs together and nesting them in lists of other metaballs. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight dent subtracted to hollow out the palm, to make the hand. +// joints = [[0,0,1], [0,0,85], [0,-5,125], [0,-16,157], [0,-30,178]]; +// finger = [ +// for(i=[0:3]) each [IDENT, mb_connector(joints[i], joints[i+1], 6+i/4, influence=.5)] +// ]; +// thumb = [ +// for(i=[0:2]) each [IDENT, mb_connector(joints[i], joints[i+1], 6+i, influence=.4)] +// ]; +// allfingers = [ +// left(25)*zrot(5)*yrot(-50)*scale([1,1,0.6])*zrot(30), thumb, +// left(15)*yrot(-9)*scale([1,1,0.9]), finger, +// IDENT, finger, +// right(15)*yrot(8)*scale([1,1,0.92]), finger, +// right(30)*yrot(17)*scale([0.9,0.9,0.75]), finger +// ]; +// hand = [ +// IDENT, allfingers, +// scale([1,0.25,1.4]), mb_cuboid(75, squareness=0.3, cutoff=80), +// move([-10,-100,55])*yrot(10)*scale([2,2,1]), +// mb_sphere(r=15, cutoff=50, influence=2, negative=true) +// ]; +// bbox = [[-100,-40,-10], [76,18,186]]; +// metaballs(hand, 2, bbox, isovalue=1, show_stats=true); module metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false) { vnf = metaballs(funcs, voxel_size, bounding_box, isovalue, closed, show_stats); @@ -1271,36 +1455,67 @@ function metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, sho 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( - nballs = len(funcs)/2, + funclist = _mb_unwind_list(funcs), + nballs = len(funclist)/2, + dummycheck = [ + for(i=[0:len(funcs)/2-1]) let(j=2*i) + assert(is_matrix(funcs[j],4,4), str("\nfuncs entry at position ", j, " must be a 4×4 matrix.")) + assert(is_function(funcs[j+1]) || is_list(funcs[j+1]), str("\nfuncs entry at position ", j+1, " must be a function literal or a metaball list.")) 0 + ], // set up transformation matrices in advance transmatrix = [ for(i=[0:nballs-1]) let(j=2*i) - assert(is_matrix(funcs[j],4,4), str("\nfuncs entry at position ", j, " must be a 4×4 matrix.")) - assert(is_function(funcs[j+1]), str("\nfuncs entry at position ", j+1, " must be a function literal.")) - transpose(select(matrix_inverse(funcs[j]), 0,2)) + transpose(select(matrix_inverse(funclist[j]), 0,2)) ], - // set up field array - bot = bounding_box[0], - top = bounding_box[1], + // new bounding box centered around original, forced to integer multiples of voxel size halfvox = 0.5*voxel_size, + bbcenter = mean(bounding_box), + bbnums = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size), + newbbox = [bbcenter - halfvox*bbnums, bbcenter + halfvox*bbnums], + + // set up field array + bot = newbbox[0], + top = newbbox[1], // accumulate metaball contributions using matrices rather than sums xset = [bot.x:voxel_size:top.x+halfvox], yset = list([bot.y:voxel_size:top.y+halfvox]), zset = list([bot.z:voxel_size:top.z+halfvox]), allpts = [for(x=xset, y=yset, z=zset) [x,y,z,1]], trans_pts = [for(i=[0:nballs-1]) allpts*transmatrix[i]], - allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funcs[2*i+1](pt)]], - total = _sum(allvals,allvals[0]*0), + allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funclist[2*i+1](pt)]], + //total = _sum(allvals,allvals[0]*EPSILON), + total = _sum(slice(allvals,1,-1), allvals[0]), fieldarray = list_to_matrix(list_to_matrix(total,len(zset)),len(yset)) - ) isosurface(fieldarray, isovalue, voxel_size, closed=closed, show_stats=show_stats, _origin=bounding_box[0]); + ) isosurface(fieldarray, isovalue, voxel_size, closed=closed, show_stats=show_stats, _mb_origin=newbbox[0]); + + +function _mb_unwind_list(list, parent_trans=[IDENT]) = + let( + dum1 = assert(is_list(list), "\nDid not find valid list of metaballs."), + n=len(list), + dum2 = assert(n%2==0, "\nList of metaballs must have an even number of elements with alternating transforms and functions/lists.") + ) [ + for(i=[0:2:n-1]) + let( + dum = assert(is_matrix(list[i],4,4), str("\nInvalid 4×4 transformation matrix found at position ",i,".")), + trans = parent_trans[0] * list[i], + j=i+1 + ) if(is_function(list[j])) + each [trans, list[j]] + else if (is_list(list[j])) + each _mb_unwind_list(list[j], [trans]) + else + assert(false, str("\nExpected function literal or list at position ",j,".")) + ]; + /// ---------- isosurface stuff starts here ---------- // Function&Module: isosurface() -// Synopsis: Creates a 3D isosurface. +// Synopsis: Creates a 3D isosurface (a 3D contour) from a function or array of values. // SynTags: Geom,VNF // Topics: Isosurfaces, VNF Generators // Usage: As a module @@ -1308,52 +1523,65 @@ function metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, sho // Usage: As a function // vnf = isosurface(f, isovalue, voxel_size, bounding_box, [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 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. +// Computes a [VNF structure](vnf.scad) of a 3D isosurface within a bounded box at a single +// isovalue or range of isovalues. +// The isosurface of a function $f(x,y,z)$ is the set of points where $f(x,y,z)=c$ for some +// constant isovalue, $c$. +// To provide a function you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// taking three parameters as input to define the grid coordinate location (e.g. `x,y,z`) and +// returning a single numerical value. +// You can also define an isosurface using a 3D array of values instead of a function, in which +// case the isosurface is the set of points where the array is equal to the isovalue. The array +// indices are in the order `[x][y][z]`. // . -// A [marching cubes](https://en.wikipedia.org/wiki/Marching_cubes) algorithm is used -// to identify an envelope containing the isosurface within the bounding box. The surface -// intersecttion with a voxel cube is then triangulated to form a surface fragment, which is -// combined with all other surface fragments. Ambiguities in triangulating the surfaces -// in certain voxel cube configurations are resolved so that all triangular facets are -// properly oriented with no holes in the surface. If a side of the bounding box clips -// the isosurface, this clipped area is filled in so that the surface remains manifold. +// The VNF that is computed has the isosurface as its bounding surface, with all the points where +// $f(x,y,z)>c$ on the interior side of the surface. +// When the isovalue is a range, `[c1, c2]`, then the resulting VNF has two bounding surfaces +// corresponding to `c1` and `c2`, and the interior of the object are the points with intermediate +// isovalues; this generally produces a shell object that has an inside and outside surface. The +// range can start at `-INF` or end at `INF`. A single isovalue `c` is equivalent to `[c,INF]`. // . -// Be mindful of how you set `voxel_size` and `bounding_box`. For example a voxel size -// of 1 unit with a bounding box volume of 200×200×200 may be noticeably slow, -// requiring calculation and storage of 8,000,000 field values, and more processing -// and memory to generate the triangulated mesh. On the other hand, a voxel size of 5 -// in a 100×100×100 bounding box requires only 8,000 field values and the mesh -// generates fairly quickly, just a handful of seconds. A good rule is to keep the -// number of field values below 10,000 for preview, and adjust the voxel size -// smaller for final rendering. If the isosurface fits completely within the bounding -// box, you can call {{pointlist_bounds()}} on `vnf[0]` returned from the -// `isosurface()` function to get an idea of a more optimal smaller bounding box to use, -// possibly allowing increasing resolution by decresing the voxel size. You can also set -// the parameter `show_stats=true` to get the bounds of the voxels containing the surface. +// The isosurface is evaluated over a bounding box which is divided into voxels of the specified +// `voxel_size`. Smaller voxels produce a finer, smoother result at the expense of execution time. +// If the voxel size doesn't exactly divide your specified bounding box, then the bounding box is +// enlarged enough to contain all whole voxels, and centered on your requested box. +// If the bounding box clips the isosurface, then if `closed=true` (default) the clipped area is +// filled in to produce a closed surface. Setting `closed=false` causes the VNF to end at the +// bounding box, resulting in a non-manifold shape with holes, exposing the inside of the object. +// . +// The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. +// A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the +// calculation and storage of 8,000,000 function values, and more processing and memory to generate +// the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box +// requires only 8,000 function values and a modest computation time. A good rule is to keep the +// number of voxels below 10,000 for preview, and adjust the voxel size smaller for final +// rendering. A bounding box that is larger than your isosurface wastes time computing function +// values that are not needed. If the isosurface fits completely within the bounding box, you can +// call {{pointlist_bounds()}} on `vnf[0]` returned from the `isosurface()` function to get an +// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the +// same run time but increase the resolution. You can also set the parameter `show_stats=true` to +// get the bounds of the voxels containing the surface. // . // 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 (often // resulting from clipping by the bounding box) 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 +// structure to {{vnf_unify_faces()}}. These steps can be computationall expensive // and are not normally necessary. // Arguments: -// f = The isosurface function. Can be a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) taking as input the `x,y,z` grid coordinates and returning a single value, or a 3D array (all points in the grid precomputed). -// **As a function literal:** 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)`. -// **As an array:** The array you pass in should be organized so that the indices are in order of `[x][y][z]` when the array is referenced; that is, `f[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. -// 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. -// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. -// bounding_box = Applicable only when `f` is a function literal. This is 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. Default: undef +// f = The isosurface function or array. +// isovalue = a scalar giving the isovalue parameter or a 2-vector giving an isovalue range. +// voxel_size = scalar size of the voxel cube that is used to sample the surface. +// bounding_box = When `f` is a function, a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. // --- -// reverse = When true, reverses the orientation of the facets in the mesh. Default: false -// 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 -// convexity = Max number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true +// reverse = When true, reverses the orientation of the VNF faces. Default: false +// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that contain the surface, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false +// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 // cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" -// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` -// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` // orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` // atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" // Anchor Types: @@ -1447,35 +1675,37 @@ function metaballs(funcs, voxel_size, bounding_box, isovalue=1, closed=true, sho // isosurface(field, isovalue=0.5, // voxel_size=10); -module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _origin=undef) { - vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, show_stats, _origin); +module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _mb_origin=undef) { + vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, show_stats, _mb_origin); vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children(); } -function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, show_stats=false, _origin=undef) = +function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, show_stats=false, _mb_origin=undef) = assert(all_defined([f, isovalue, voxel_size]), "\nThe parameters f, isovalue, and bounding_box must all be defined.") assert((is_function(f) && is_def(bounding_box)) || (is_list(f) && is_undef(bounding_box)), "\nbounding_box must be passed if f is a function, and cannot be passed if f is an array.") let( isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, isovalmax = is_list(isovalue) ? isovalue[1] : INF, + dum1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")), + hv = 0.5*voxel_size, bbox = is_function(f) - ? let( // new bounding box quantized for voxel_size - hv = 0.5*voxel_size, - 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] - : let( - nx = len(f)-1, - ny = len(f[0])-1, - nz = len(f[0][0])-1 - ) is_def(_origin) ? [_origin, _origin+voxel_size*[nx,ny,nz]] - : [-0.5*voxel_size*[nx,ny,nz], 0.5*voxel_size*[nx, ny, nz]], - cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, isovalmin=isovalmin, isovalmax=isovalmax, closed=closed), + ? let( // new bounding box quantized for voxel_size, centered around original box + bbcenter = mean(bounding_box), + bbn = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size) + ) [bbcenter - hv*bbn, bbcenter + hv*bbn] + : let( // new bounding box, either centered on origin or using metaball origin + dims = list_shape(f) - [1,1,1] + ) is_def(_mb_origin) + ? [_mb_origin, _mb_origin+voxel_size*dims] // metaball bounding box + : [-hv*dims, hv*dims], // centered bounding box + cubes = _isosurface_cubes(voxel_size, bbox, + fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, + 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), faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], - dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 + dum2 = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 ) [trianglepoints, faces]; From d94b0bf52d1666cf0e2308f474532bc803a6cc96 Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Tue, 11 Feb 2025 12:52:05 -0800 Subject: [PATCH 07/11] Update isosurface.scad --- isosurface.scad | 589 ++++++++++++++++++++++++++++++------------------ 1 file changed, 366 insertions(+), 223 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index b365185b..c45c4f14 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,9 +1,9 @@ ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), -// are bounded and closed organic surfaces that smoothly blend together. -// Metaballs are one specific kind of isosurface. -// . +// are bounded and closed organic surfaces that smoothly blend together. +// Metaballs are one specific kind of isosurface. +// . // An isosurface, or implicit surface, is a three-dimensional surface representing all points of a // constant 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 @@ -19,14 +19,14 @@ // fashion. The resulting metaball model appears as smoothly blended blobby shapes. The // implementation below provides metaballs of a variety of types including spheres, cuboids and // cylinders (cones), with optional parameters to adjust the influence of one metaball on others, -// and the cutoff distance where the metaball's influence stops. +// and the cutoff distance where the metaball's influence stops. // . // An isosurface can be defined using any function of three variables: // 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". // Changing the isovalue will tend to grow or shrink the isosurface, depending on how the function is // defined. Since metaballs are isosurfaces, they also have an isovalue. The isovalue is also known -// as the "threshold". +// as the "threshold". // . // Some isosurface functions are unbounded, extending infinitely in all directions. A familiar example may // be a [gryoid](https://en.wikipedia.org/wiki/Gyroid), which is often used as a volume infill pattern in @@ -723,55 +723,84 @@ function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isov cubefound_outer = len(bfaces)==0 ? false : let( bf = flatten([for(i=bfaces) _MCFaceVertexIndices[i]]), - sumcond = sum([for(b=bf) isovalmin<=cf[b] && cf[b]<=isovalmax ? 1 : 0]) + sumcond = len([for(b=bf) if(isovalmin<=cf[b] && cf[b]<=isovalmax) 1 ]) ) sumcond == len(bf), cubeindex_isomin = cubefound_isomin ? _cubeindex(cf, isovalmin) : 0, cubeindex_isomax = cubefound_isomax ? _cubeindex(cf, isovalmax) : 0 - ) if(cubefound_isomin || cubefound_isomax || cubefound_outer) [ - cubecoord, - cubeindex_isomin, cubeindex_isomax, - cf, bfaces - ] + ) if(cubefound_isomin || cubefound_isomax || cubefound_outer) + [ // return data structure: + cubecoord, // voxel lower coordinate + cubeindex_isomin, cubeindex_isomax, // cube IDs for isomin and isomax + [for(f=cf) min(1e9, max(-1e9, f))], // clamped voxel corner values + bfaces // list of bounding box faces, if any + ] ]; /// _isosurface_trangles() - called by isosurface() /// Given a list of voxel cubes structures, triangulate the isosurface(s) that intersect each cube and return a list of triangle vertices. function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritablemin, tritablemax) = [ - for(cl=cubelist) let( - v = cl[0], - cbidxmin = cl[1], - cbidxmax = cl[2], - f = cl[3], - bbfaces = cl[4], - vcube = [ - v, v+[0,0,cubesize], v+[0,cubesize,0], v+[0,cubesize,cubesize], - v+[cubesize,0,0], v+[cubesize,0,cubesize], - v+[cubesize,cubesize,0], v+[cubesize,cubesize,cubesize] - ], - epathmin = tritablemin[cbidxmin], - epathmax = tritablemax[cbidxmax], - lenmin = len(epathmin), - lenmax = len(epathmax), - outfacevertices = flatten([ - for(bf = bbfaces) - _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) - ]), - n_outer = len(outfacevertices) - ) - // some repeated code here in an attempt to gain some speed to avoid function calls and calls to flatten(). - // Where the face of the bounding box clips a voxel, those are done in separate if() blocks and require require a concat(), but the majority of voxels can have triangles generated directly. If there is no clipping, the list of trianges is generated all at once. - if(lenmin>0 && lenmax>0) let( + for(cl=cubelist) + let( + v = cl[0], + cbidxmin = cl[1], + cbidxmax = cl[2], + f = cl[3], + bbfaces = cl[4], + vcube = [ + v, v+[0,0,cubesize], v+[0,cubesize,0], v+[0,cubesize,cubesize], + v+[cubesize,0,0], v+[cubesize,0,cubesize], + v+[cubesize,cubesize,0], v+[cubesize,cubesize,cubesize] + ], + epathmin = tritablemin[cbidxmin], + epathmax = tritablemax[cbidxmax], + lenmin = len(epathmin), + lenmax = len(epathmax), + outfacevertices = flatten([ + for(bf = bbfaces) + _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) + ]), + n_outer = len(outfacevertices) + ) + +/* // this block of commented code shorter and easier to read, but a bit slower to execute + each [ + if(lenmin>0) for(ei=epathmin) // min surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(lenmax>0) for(ei=epathmax) // max surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(n_outer>0) for(bf = bbfaces) + each _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) + ] +]; +*/ + // Some repeated code here, marginally faster (3% or so) than the commented section above. + // Where the face of the bounding box clips a voxel, those are done in separate if() blocks and require a 'each', but the majority of voxels can have triangles generated directly. If there is no clipping, the list of trianges is generated all at once. // both min and max surfaces intersect a voxel clipped by bounding box - list = concat( + if(lenmin>0 && lenmax>0) + each concat( // min surface [ for(ei=epathmin) let( edge = _MCEdgeVertexIndices[ei], vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], // max surface [ for(ei=epathmax) let( @@ -779,57 +808,57 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(n_outer>0 && lenmin>0) let( + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices + ) // only min surface intersects a voxel clipped by bounding box - list = concat( + else if(n_outer>0 && lenmin>0) + each concat( [ for(ei=epathmin) let( edge = _MCEdgeVertexIndices[ei], vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(lenmin>0) + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices + ) // only min surface intersects a voxel + else if(lenmin>0) for(ei=epathmin) let( edge = _MCEdgeVertexIndices[ei], vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmin-f[vi0]) / denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) - else if(n_outer>0 && lenmax>0) let( // only max surface intersects the voxel on the bounding box - list = concat( + else if(n_outer>0 && lenmax>0) + each concat( [ for(ei=epathmax) let( edge = _MCEdgeVertexIndices[ei], vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom - ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) - ) for(ls = list) ls - else if(lenmax>0) + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices + ) // only max surface intersects the voxel + else if(lenmax>0) for(ei=epathmax) let( edge = _MCEdgeVertexIndices[ei], vi0 = edge[0], vi1 = edge[1], denom = f[vi1] - f[vi0], - u = abs(denom)<0.0001 || denom==-INF ? 0.5 : (isovalmax-f[vi0]) / denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) - else if(n_outer>0) // no surface intersects a voxel clipped by bounding box but the bounding box at this voxel is inside the volume between isomin and isomax - for(ls = outfacevertices) ls + else if(n_outer>0) + each outfacevertices ]; @@ -849,20 +878,20 @@ function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( fmax = max(f0, f1), fbetweenlow = (fmin <= isovalmin && isovalmin <= fmax), fbetweenhigh = (fmin <= isovalmax && isovalmax <= fmax), - denom = f1==INF || f0==INF ? 0 : f1-f0 + denom = f1-f0 ) [ if(isovalmin <= f0 && f0 <= isovalmax) vcube[vi0], if(fbetweenlow && f0<=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f0)/denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f0)/denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), if(fbetweenhigh && f0<=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f0)/denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f0)/denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), if(fbetweenhigh && f0>=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f0)/denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f0)/denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), if(fbetweenlow && f0>=f1) let( - u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f0)/denom + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f0)/denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ] @@ -900,33 +929,9 @@ function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( /// Animated metaball demo made with BOSL2 here: https://imgur.com/a/m29q8Qd /// Built-in metaball functions corresponding to each MB_ index. -/// Each function takes three parameters: -/// dv = cartesian distance, a vector [dx,dy,dz] being the distances from the ball center to the volume sample point -/// coeff = the intensity (weight, charge, density, etc.) of the metaball, can be a vector if warranted. -/// additional value or array of values needed by the function. -/// cutoff = radial cutoff distance; effect suppression increases with distance until zero at the cutoff distance, and is zero from that point farther out. Default: INF -/// influence = inverse exponent to 1/r, the higher the influence, the further the "reach" of the metaball. Default: 1 +/// For speed, they are split into four functions, each handling a different combination of influence != 1 or influence == 1, and cutoff < INF or cutoff == INF. -/// metaball field function, calling any of the other metaball functions above to accumulate -/// the contribution of each metaball at point xyz - -/* -/// metaball suppression and cutoff function to control shape of the falloff -function mb_shaping(dist, coeff, cutoff, influence) = - sign(influence) * 0.5*(cos(180*(dist/cutoff)^4)+1) - * (coeff/dist)^(1/abs(influence)); - - -function _mb_sphere(dv, coeff, cutoff, influence) = - let(r=norm(dv)) r>=cutoff ? 0 - : mb_shaping(r, coeff, cutoff, influence); - - -//function mb_sphere(coeff=10, cutoff=INF, influence=1) = function (dv) _mb_sphere(dv, coeff, cutoff, influence); -*/ - - -/// metaball cutoff function +/// public metaball cutoff function if anyone wants it (demonstrated in example) function mb_cutoff(dist, cutoff) = dist>=cutoff ? 0 : 0.5*(cos(180*(dist/cutoff)^4)+1); @@ -936,7 +941,7 @@ function _mb_sphere_basic(dv, r, neg) = neg*r/norm(dv); function _mb_sphere_influence(dv, r, ex, neg) = neg * (r/norm(dv))^ex; function _mb_sphere_cutoff(dv, r, cutoff, neg) = let(dist=norm(dv)) neg * mb_cutoff(dist, cutoff) * r/dist; -function _mb_sphere_full(dv, r, ex, cutoff, neg) = let(dist=norm(dv)) +function _mb_sphere_full(dv, r, cutoff, ex, neg) = let(dist=norm(dv)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = @@ -950,7 +955,182 @@ function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = !is_finite(cutoff) && influence==1 ? function(dv) _mb_sphere_basic(dv,r,neg) : !is_finite(cutoff) ? function(dv) _mb_sphere_influence(dv,r,1/influence, neg) : influence==1 ? function(dv) _mb_sphere_cutoff(dv,r,cutoff,neg) - : function(dv) _mb_sphere_full(dv,r,1/influence,cutoff,neg); + : function(dv) _mb_sphere_full(dv,r,cutoff,1/influence,neg); + +// metaball rounded cube + +function _mb_cuboid_basic(dv, siz, xp, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg*siz/dist; +function _mb_cuboid_influence(dv, siz, xp, ex, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * (siz/dist)^ex; +function _mb_cuboid_cutoff(dv, siz, xp, cutoff, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) * siz/dist; +function _mb_cuboid_full(dv, siz, xp, cutoff, ex, neg) = let( + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) * (siz/dist)^ex; + +function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + assert(is_finite(size) && size>0, "\nsize must be a positive number.") + let( + dummy=assert(is_num(size) && size>0, "\ninvalid size."), + xp = _squircle_se_exponent(squareness), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, size/2, xp, neg) + : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, size/2, xp, 1/influence, neg) + : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, size/2, xp, cutoff, neg) + : function (dv) _mb_cuboid_full(dv, size/2, xp, cutoff, 1/influence, neg); + +// metaball rounded newcube that takes a 3-vector as size + +function _mb_newcuboid_basic(dv, inv_size, xp, neg) = + let( + dv=inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) + ) neg/dist; +function _mb_newcuboid_influence(dv, inv_size, xp, ex, neg) = let( + dv=inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg / dist^ex; +function _mb_newcuboid_cutoff(dv, inv_size, xp, cutoff, neg) = let( + dv = inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist; +function _mb_newcuboid_full(dv, inv_size, xp, ex, cutoff, neg) = let( + dv = inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist^ex; + +function mb_newcuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + assert((is_finite(size) && size>0) || (is_vector(size) && all_positive(size)), "\nsize must be a positive number or a 3-vector of positive values.") + let( + xp = _squircle_se_exponent(squareness), + neg = negative ? -1 : 1, + inv_size = is_num(size) ? 2/size + : [[2/size.x,0,0],[0,2/size.y,0],[0,0,2/size.z]] + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, inv_size, xp, neg) + : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, inv_size, xp, 1/influence, neg) + : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, inv_size, xp, cutoff, neg) + : function (dv) _mb_cuboid_full(dv, inv_size, xp, 1/influence, cutoff, neg); + + +// metaball rounded cylinder / cone + +function _revsurf_basic(dv, path, coef, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] ? coef*(1+dist) : coef/(1+dist)); + +function _revsurf_influence(dv, path, coef, exp, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] ? (coef*(1+dist))^exp : (coef/(1+dist))^exp); + +function _revsurf_cutoff(dv, path, coef, cutoff, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] + ? (coef*(1+dist)) : mb_cutoff(dist-coef, cutoff) * (coef/(1+dist)) ); + +function _revsurf_full(dv, path, coef, cutoff, exp, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] + ? (coef*(1+dist))^exp : mb_cutoff(dist-coef, cutoff) * (coef/(1+dist))^exp ); + +function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influence=1, negative=false) = + let( + r1 = get_radius(r1=r1,r=r, d1=d1, d=d), + r2 = get_radius(r1=r2,r=r, d1=d2, d=d), + h = first_defined([h,l,height,length],"h,l,height,length") + ) + assert(is_finite(rounding) && rounding>=0, "rounding must be a nonnegative number") + assert(is_finite(r1) && r1>0, "r/r1/d/d1 must be a positive number") + assert(is_finite(r2) && r2>0, "r/r2/d/d2 must be a positive number") + let( + vang = atan2(r1-r2,h), + facelen = adj_ang_to_hyp(h, abs(vang)), + roundlen1 = rounding/tan(45-vang/2), + roundlen2 = rounding/tan(45+vang/2), + sides = [[0,h/2], [r2,h/2], [r1,-h/2], [0,-h/2]], + neg = negative ? -1 : 1 + ) + assert(roundlen1 <= r1, "size of rounding is larger than the r1 radius of the cylinder/cone") + assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") + assert(roundlen1+roundlen2 < facelen, "Roundings don't fit on the edge length of the cylinder/cone") + let(shifted = offset(sides, delta=-rounding, closed=false)) + !is_finite(cutoff) && influence==1 ? function(dv) _revsurf_basic(dv, shifted, 1+rounding, neg) + : !is_finite(cutoff) ? function(dv) _revsurf_influence(dv, shifted, 1+rounding, 1/influence, neg) + : influence==1 ? function(dv) _revsurf_cutoff(dv, shifted, 1+rounding, cutoff, neg) + : function (dv) _revsurf_full(dv, shifted, 1+rounding, cutoff, 1/influence, neg); // metaball capsule (round-ended cylinder) @@ -966,7 +1146,7 @@ function _mb_capsule_cutoff(dv, hl, r, cutoff, neg) = let( dist = dv.z<-hl ? norm(dv-[0,0,-hl]) : dv.z0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + h2 = h/2, + or = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && or>0, "\ninvalid radius or diameter."), + r = or - h2, + dum3 = assert(r>0, "\nDiameter must be greater than height."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_disk_basic(dv,h2,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_disk_influence(dv,h2,r,1/influence, neg) + : influence==1 ? function(dv) _mb_disk_cutoff(dv,h2,r,cutoff,neg) + : function (dv) _mb_disk_full(dv, h2, r, cutoff, 1/influence, neg); // metaball connector cylinder - calls mb_capsule* functions after transform @@ -994,10 +1215,11 @@ function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") let( - dum1 = assert(is_matrix([p1],1,3), "\nConnector start point p1 must be a 3D coordinate."), - dum2 = assert(is_matrix([p2],1,3), "\nConnector end point p2 must be a 3D coordinate."), + dum1 = assert(is_vector(p1,3), "\nConnector start point p1 must be a 3D coordinate.") + assert(is_vector(p2,3), "\nConnector end point p2 must be a 3D coordinate.") + assert(p1 != p2, "\nStart and end points p1 and p2 cannot be the same."), r = get_radius(r=r,d=d), - dum3 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), neg = negative ? -1 : 1, dc = p2-p1, // center-to-center distance midpt = reverse(-0.5*(p1+p2)), @@ -1015,87 +1237,7 @@ function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = _mb_capsule_cutoff(newdv,h,r,cutoff,neg) : function (dv) let(newdv = transform * [each dv,1]) - _mb_capsule_full(newdv, h, r, 1/influence, cutoff, neg); - -// metaball rounded cylinder / cone - -function revsurf(dv, path, coef, exp, cutoff) = - let( - pt = [norm([dv.x,dv.y]), dv.z], - segs = pair(path), - dist = min([for(seg=segs) - let( - c=seg[1]-seg[0], - s0 = seg[0]-pt, - t = -s0*c/(c*c) - ) - t<0 ? norm(s0) - : t>1 ? norm(seg[1]-pt) - : norm(s0+t*c)]), - inside_check = [for(seg=segs) - if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] - ) - mb_cutoff(dist, cutoff) * (inside_check==[] - ? (coef*(1+dist))^exp : (coef/(1+dist))^exp ); - -function revsurf_rounded(path, rounding, cutoff, influence) = - let(shifted = offset(path, delta=-rounding, closed=false)) - function (dv) revsurf(dv, shifted, 1+rounding, 1/influence, cutoff); - -function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influence=1, negative=false) = - let( - r1 = get_radius(r1=r1,r=r, d1=d1, d=d), - r2 = get_radius(r1=r2,r=r, d1=d2, d=d), - h = first_defined([h,l,height,length],"h,l,height,length") - ) - assert(is_finite(rounding) && rounding>=0, "rounding must be a nonnegative number") - assert(is_finite(r1) && r1>0, "r/r1/d/d1 must be a positive number") - assert(is_finite(r2) && r2>0, "r/r2/d/d2 must be a positive number") - let( - vang = atan2(r1-r2,h), - facelen = adj_ang_to_hyp(h, abs(vang)), - roundlen1 = rounding/tan(45-vang/2), - roundlen2 = rounding/tan(45+vang/2), - sides = [[0,h/2],[r2,h/2],[r1,-h/2], [0, -h/2]], - ) - assert(roundlen1 <= r1, "size of rounding is larger than the r1 radius of the cylinder/cone") - assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") - assert(roundlen1+roundlen2 <=facelen, "Roundings don't fit on the edge length of the cylinder/cone") - revsurf_rounded(sides, rounding, influence); - - -// metaball rounded cube - -function _mb_cuboid_basic(dv, siz, xp, neg) = let( - dist = xp >= 1100 ? max(v_abs(dv)) - : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) -) neg*siz/dist; -function _mb_cuboid_influence(dv, siz, xp, ex, neg) = let( - dist = xp >= 1100 ? max(v_abs(dv)) - :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) -) neg * (siz/dist)^ex; -function _mb_cuboid_cutoff(dv, siz, xp, cutoff, neg) = let( - dist = xp >= 1100 ? max(v_abs(dv)) - : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) -) neg * mb_cutoff(dist, cutoff) * siz/dist; -function _mb_cuboid_full(dv, siz, xp, ex, cutoff, neg) = let( - dist = xp >= 1100 ? max(v_abs(dv)) - :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) -) neg * mb_cutoff(dist, cutoff) * (siz/dist)^ex; - -function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = - assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") - assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") - assert(is_finite(size) && size>0, "\nsize must be a positive number.") - let( - dummy=assert(is_num(size) && size>0, "\ninvalid size."), - xp = _squircle_se_exponent(squareness), - neg = negative ? -1 : 1 - ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, size/2, xp, neg) - : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, size/2, xp, 1/influence, neg) - : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, size/2, xp, cutoff, neg) - : function (dv) _mb_cuboid_full(dv, size/2, xp, 1/influence, cutoff, neg); + _mb_capsule_full(newdv, h, r, cutoff, 1/influence, neg); // metaball octahedron @@ -1105,7 +1247,7 @@ function _mb_octahedron_influence(dv, r, ex, neg) = let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * (r/dist)^ex; function _mb_octahedron_cutoff(dv, r, cutoff, neg) = let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * r/dist; -function _mb_octahedron_full(dv, r, ex, cutoff, neg) = +function _mb_octahedron_full(dv, r, cutoff, ex, neg) = let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = @@ -1119,7 +1261,7 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = !is_finite(cutoff) && influence==1 ? function(dv) _mb_octahedron_basic(dv,r,neg) : !is_finite(cutoff) ? function(dv) _mb_octahedron_influence(dv,r,1/influence, neg) : influence==1 ? function(dv) _mb_octahedron_cutoff(dv,r,cutoff,neg) - : function(dv) _mb_octahedron_full(dv,r,1/influence,cutoff,neg); + : function(dv) _mb_octahedron_full(dv,r,cutoff,1/influence,neg); // torus @@ -1130,7 +1272,7 @@ function _mb_torus_influence(dv, rmaj, rmin, ex, neg) = function _mb_torus_cutoff(dv, rmaj, rmin, cutoff, neg) = let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * mb_cutoff(dist, cutoff) * rmin/dist; -function _mb_torus_full(dv, rmaj, rmin, ex, cutoff, neg) = +function _mb_torus_full(dv, rmaj, rmin, cutoff, ex, neg) = let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; @@ -1156,19 +1298,7 @@ function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, !is_finite(cutoff) && influence==1 ? function(dv) _mb_torus_basic(dv, r_maj, r_min, neg) : !is_finite(cutoff) ? function(dv) _mb_torus_influence(dv, r_maj, r_min, 1/influence, neg) : influence==1 ? function(dv) _mb_torus_cutoff(dv, r_maj, r_min, cutoff, neg) - : function(dv) _mb_torus_full(dv, r_maj, r_min, 1/influence, cutoff, neg); - - - -/* hard-edge cylinder -_mb_capsule = function (dv, coeff, length, cutoff, influence) -let( - dist = max(abs(dv.z*2*coeff/length), norm([dv.x,dv.y])), - suppress = let(a = min(r,cutoff)/cutoff) 1-a*a, -) suppress*coeff / dist; - -function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv) _mb_capsule(dv, coeff, length, cutoff, influence); -*/ + : function(dv) _mb_torus_full(dv, r_maj, r_min, cutoff, 1/influence, neg); // Function&Module: metaballs() @@ -1198,16 +1328,18 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // write `up(5) xrot(25) zrot(45) scale(4)`. You would provide that transformation // as the transformation matrix `up(5) * xrot(25) * zrot(45) * scale(4)`. You can use // scaling to produce an ellipse from a sphere, and you can even use {{skew()}} if desired. -// When no transformation is needed, give `IDENT` as the transformation. +// When no transformation is needed, give `IDENT` as the transformation. // . // You can create metaballs in a variety of standard shapes using the predefined functions // listed below. If you wish, you can also create custom metaball shapes using your own functions -// (see Example 11). Three parameters are available on all of the built-in metaballs to control the +// (see Example 12). Three parameters are available on all of the built-in metaballs to control the // interaction of the metaballs with each other: `cutoff`, `influence`, and `negative'. // . // The `cutoff` parameter specifies the distance beyond which the metaball has no interaction with // other balls. When you apply `cutoff`, a smooth suppression factor begins begins decreasing the // interaction strength at half the cutoff distance and reduces the interaction to zero at the cutoff. +// The smooth decrease may cause the influence to appear negligible slightly before the cutoff distance +// distance specified, depending on the voxel size and influence of the ball. // . // The `influence` parameter adjusts the strength of the interaction metaball objects have with each // other. If you increase `influence` from its default of 1, the metaball interacts with other @@ -1221,7 +1353,7 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // The `negative` parameter, if set to `true`, creates a negative metaball, which can create // hollows or dents in other metaballs, or swallow other metaballs entirely, making them disappear. // Negative metaballs are always below the isovalue, so they are never directly visible; -// only their effects are visible. See Example 7. +// only their effects are visible. See Examples 7 and 8. // . // For complicated metaball assemblies you may wish to repeat a structure in different locations or // otherwise transformed. Nesting metaball specifications are supported: @@ -1230,7 +1362,7 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // `hand=[u0,finger,u1,finger,...]` and then invoke metaball with `[s0, hand]`. // Basically, any list of metaballs is, itself, a metaball. It can be used in place of a function in another list. // This is a powerful technique that lets you make groups of metaballs that you can use as individual -// metaballs in other groups, and can make your code compact and simpler to understand. See Example 13. +// metaballs in other groups, and can make your code compact and simpler to understand. See Example 14. // . // .h3 Built-in metaball functions // Several metaballs are defined for you to use in your models. @@ -1243,35 +1375,37 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // if `isovalue<1` and smaller than their specified sizes if `isovalue>1`. // . // All of the built-in functions all accept these named arguments, which are not repeated in the list below: -// * `cutoff` - specifies the distance beyond which the metaball has no influence. Default: INF -// * `influence` - a positive number that determines how much influence the metaball has on others. Default: 1 -// * `negative` - when true, causes the metaball to have a negative influence on its surroundings. Default: false +// * `cutoff` - specifies the distance beyond which the metaball has no influence. **If you scale the metaball, the cutoff gets scaled also. Because cutoff is a smoothly decreasing function, the influence may effectively disappear slightly before the cutoff distance distance specified, depending on the voxel size and influence of the ball. ** Depending on the value of `influence`, a cutoff that ends in the middle of another ball can result in strange shapes, as shown in Example 8, with the metaball being simultaneously influenced and not influenced on either side of the boundary. Default: INF +// * `influence` - a positive number that determines how much influence the metaball has on others. Default: 1 +// * `negative` - when true, causes the metaball to have a negative influence on its surroundings. Default: false // . // The built-in metaball functions are: // * `mb_sphere(r|d=)` - spherical metaball, with radius r or diameter d. You can create an ellipsoid using `scale()` as the last transformation entry of the metaball `funcs` array. -// * `mb_cuboid(size, [squareness=0.5])` - cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` specifies the dimensions of the cuboid shape between the face centers, but except when `squareness=1`, the faces are always a little bit curved. -// * `mb_cyl(h|l|height|length=, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=0])` - vertical cylinder or cone metaball with the same dimenional arguments as {{cyl()}}. The rounding is the same at both ends. If you just want a cylindrical shape, consider `mb_capsule()`, which has a faster execution time. -// * `mb_capsule(h|l|height|length=, r|d=)` - vertical cylinder of radius `r` or diameter `d` with hemispherical caps. The height or length specifies the **total** height including the rounded ends. -// * `mb_connector(p1, p2, r|d=)` - cylinder of radius `r` or diameter `d` with hemispherical caps (like mb_capsule), but specified to connect point `p1` to point `p2` (where `p1` and `p2` are 3D vectors). The specified points are at the ends of the straight portion of the shape (the centers of the two capping hemispheres). -// * `mb_octahedron(r|d=])` - octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip. The vertex parameter specifies the distance between the two opposite tips. -// * `mb_torus([r_maj|d_maj=], [r_min|d_min=],[or=|od=],[ir=|id=])` - torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. +// * `mb_cuboid(size, [squareness=0.5])` - cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` is a scalar that specifies the width of the cuboid shape between the face centers. Except when `squareness=1`, the faces are always a little bit curved. +// * `mb_cyl(h|l|height|length=, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=0])` - vertical cylinder or cone metaball with the same dimenional arguments as {{cyl()}}. Only one rounding value is allowed; the rounding is the same at both ends. If you just want a cylindrical shape, consider `mb_capsule()` or `mb_disc()`, which hav faster execution times. +// * `mb_capsule(h|l|height|length=, r|d=)` - vertical cylinder of radius `r` or diameter `d` with hemispherical caps. The height or length specifies the **total** height including the rounded ends. +// * `mb_disk(h|l|height|length=, [r|d=], [r1=|d1=], [r2=|d2=])` - This is the complement of `mb_capsule()` but instead of a cylinder with round ends, this is a disk with flat ends and rounded sides. The diameter you specify must be greater than its height. +// * `mb_connector(p1, p2, r|d=)` - cylinder of radius `r` or diameter `d` with hemispherical caps (like mb_capsule), but specified to connect point `p1` to point `p2` (where `p1` and `p2` must be different 3D vectors). The specified points are at the ends of the straight portion of the shape (the centers of the two capping hemispheres). +// * `mb_octahedron(r|d=])` - octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip. The vertex parameter specifies the distance between the two opposite tips. +// * `mb_torus([r_maj|d_maj=], [r_min|d_min=],[or=|od=],[ir=|id=])` - torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. // . // .h3 Metaball functions and user defined functions // Each metaball function is defined as a function of a 3-vector that gives the value of the metaball function // for that point in space. As is common in metaball implementations, we define the built-in metaballs using an -// inverse relationship where the metaball functions fall off as $1/d$. The spherical metaball therefore has -// a simple basic definition as `f(v) = 1/norm(v)`. Note that with this framework, `f(v)>=c` defines a bounded -// object. Increasing the isovalue shrinks the object, and decreasing the isovalue grows the object. +// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance from the +// metaball center. The spherical metaball therefore has a simple basic definition as `f(v) = 1/norm(v)`. +// Note that with this framework, `f(v) >= c` defines a bounded object. Increasing the isovalue shrinks the +// object, and decreasing the isovalue grows the object. // . -// In order to adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` then the -// decay becomes $1/d^(1/a)$. This means, for example, that if you set influence to 2 you get a $1/d^2$ falloff. -// Changing this exponent changes how the balls interact. +// In order to adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` +// then the decay becomes $1/d^(1/a)$. This means, for example, that if you set influence to 2 you get a +// $1/d^2$ falloff. Changing this exponent changes how the balls interact. // . // You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) // that takes a single argument (a 3-vector) and returns a single numerical value. -// The returned value should define a function where in isovalue range [c,INF] defines a bounded object. See Example 11. +// The returned value should define a function where in isovalue range [c,INF] defines a bounded object. See Example 12. // Arguments: -// funcs = a 1-D list of transform and function pairs in the form `[trans0, func0, trans1, func1, ...]`, with one pair for each metaball. The transform should be at least a position such as `move([x,y,z])` to specify the location of the metaball center, but you can also include rotations, such as `move([x,y,z])*rot([ax,ay,az])`. You can multiply together any of BOSL2's affine operations like {{xrot()}}, {{scale()}}, and {{skew()}}. This is useful for orienting non-spherical metaballs. The priority order of the transforms is right to left, that is, `move([4,5,6])*rot([45,0,90])` does the rotation first, and then the move, similar to normal OpenSCAD syntax `translate([4,5,6]) rotate([45,0,90]) children()`. The transform `IDENT` may be used if you don't want to specify a transform, resulting in a metaball positioned at the origin. **Any function in the list may, itself, be another list of metaballs instead of a function name.** See Example 13 for a demonstration. +// funcs = a 1-D list of transform and function pairs in the form `[trans0, func0, trans1, func1, ...]`, with one pair for each metaball. The transform should be at least a position such as `move([x,y,z])` to specify the location of the metaball center, but you can also include rotations, such as `move([x,y,z])*rot([ax,ay,az])`. You can multiply together any of BOSL2's affine operations like {{xrot()}}, {{scale()}}, and {{skew()}}. This is useful for orienting non-spherical metaballs. The priority order of the transforms is right to left, that is, `move([4,5,6])*rot([45,0,90])` does the rotation first, and then the move, similar to normal OpenSCAD syntax `translate([4,5,6]) rotate([45,0,90]) children()`. The transform `IDENT` may be used if you don't want to specify a transform, resulting in a metaball positioned at the origin. **Any function in the list may, itself, be another list of metaballs instead of a function name.** See Example 14 for a demonstration. // voxel_size = The size (scalar) of the voxel cube that determines the resolution of the metaball surface. **Start with a larger size for experimenting, and refine it gradually.** A small voxel size can significantly slow down processing time, especially with a large `bounding_box`. // bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The voxels needn't fit perfectly inside the bounding box. // isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 @@ -1359,6 +1493,15 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // boundingbox = [[-7,-6,-6], [3,6,6]]; // #metaballs(funcs, voxelsize, boundingbox, isovalue); // color("green") move_copies(centers) sphere(d=1, $fn=16); +// Example(3D,VPD=100): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball as high influence and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin. +// funcs = [ +// back(10), mb_sphere(20), +// IDENT, mb_sphere(2, influence = 30, cutoff = 20, negative = true), +// ]; +// isovalue = 1; +// voxelsize = 0.5; +// boundingbox = [[-20,-4,-20], [20,30,20]]; +// metaballs(funcs, voxelsize, boundingbox, isovalue); // Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. // funcs = [ // move([-7,-3,27])*zrot(55), mb_cuboid(6, squareness=1), @@ -1421,16 +1564,16 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // // useful to save as VNF for copies and manipulations // vnf = metaballs(funcs, voxelsize, boundingbox, isovalue=1); // vnf_polyhedron(vnf); -// Example(3D,Med,NoAxes): Example of grouping metaballs together and nesting them in lists of other metaballs. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight dent subtracted to hollow out the palm, to make the hand. +// Example(3D,Med,NoAxes,VPR=[70,0,30],VPD=520,VPT=[0,0,80]): Example of grouping metaballs together and nesting them in lists of other metaballs. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight ellipsoid dent subtracted to hollow out the palm, to make the hand. // joints = [[0,0,1], [0,0,85], [0,-5,125], [0,-16,157], [0,-30,178]]; // finger = [ -// for(i=[0:3]) each [IDENT, mb_connector(joints[i], joints[i+1], 6+i/4, influence=.5)] +// for(i=[0:3]) each [IDENT, mb_connector(joints[i], joints[i+1], 9+i/5, influence=.3)] // ]; // thumb = [ -// for(i=[0:2]) each [IDENT, mb_connector(joints[i], joints[i+1], 6+i, influence=.4)] +// for(i=[0:2]) each [scale([1,1,1.2]), mb_connector(joints[i], joints[i+1], 9+i/2, influence=.3)] // ]; // allfingers = [ -// left(25)*zrot(5)*yrot(-50)*scale([1,1,0.6])*zrot(30), thumb, +// left(15)*zrot(5)*yrot(-50)*scale([1,1,0.6])*zrot(30), thumb, // left(15)*yrot(-9)*scale([1,1,0.9]), finger, // IDENT, finger, // right(15)*yrot(8)*scale([1,1,0.92]), finger, @@ -1438,8 +1581,8 @@ function mb_capsule(coeff=10, length=10, cutoff=INF, influence=1) = function (dv // ]; // hand = [ // IDENT, allfingers, -// scale([1,0.25,1.4]), mb_cuboid(75, squareness=0.3, cutoff=80), -// move([-10,-100,55])*yrot(10)*scale([2,2,1]), +// move([-5,0,5])*scale([1,0.3,1.4]), mb_cuboid(90, squareness=0.3, cutoff=80), +// move([-10,-95,50])*yrot(10)*scale([2,2,0.95]), // mb_sphere(r=15, cutoff=50, influence=2, negative=true) // ]; // bbox = [[-100,-40,-10], [76,18,186]]; @@ -1532,14 +1675,14 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // returning a single numerical value. // You can also define an isosurface using a 3D array of values instead of a function, in which // case the isosurface is the set of points where the array is equal to the isovalue. The array -// indices are in the order `[x][y][z]`. +// indices are in the order `[x][y][z]`. // . // The VNF that is computed has the isosurface as its bounding surface, with all the points where // $f(x,y,z)>c$ on the interior side of the surface. // When the isovalue is a range, `[c1, c2]`, then the resulting VNF has two bounding surfaces // corresponding to `c1` and `c2`, and the interior of the object are the points with intermediate // isovalues; this generally produces a shell object that has an inside and outside surface. The -// range can start at `-INF` or end at `INF`. A single isovalue `c` is equivalent to `[c,INF]`. +// range can start at `-INF` or end at `INF`. A single isovalue `c` is equivalent to `[c,INF]`. // . // The isosurface is evaluated over a bounding box which is divided into voxels of the specified // `voxel_size`. Smaller voxels produce a finer, smoother result at the expense of execution time. @@ -1571,9 +1714,9 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // and are not normally necessary. // Arguments: // f = The isosurface function or array. -// isovalue = a scalar giving the isovalue parameter or a 2-vector giving an isovalue range. +// isovalue = a scalar giving the isovalue parameter or a 2-vector giving an isovalue range. // voxel_size = scalar size of the voxel cube that is used to sample the surface. -// bounding_box = When `f` is a function, a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. +// bounding_box = When `f` is a function, a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. // --- // closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true // reverse = When true, reverses the orientation of the VNF faces. Default: false @@ -1616,7 +1759,7 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // isovalue = [-0.3, 0.3]; // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), -// isovalue, voxel_size=5, bounding_box=bbox], +// isovalue, voxel_size=5, bounding_box=bbox, // 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( From e86e92c2035f10ec9ae72dd1204550452981515e Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Thu, 13 Feb 2025 10:57:36 -0800 Subject: [PATCH 08/11] update isosurface.scad --- isosurface.scad | 1855 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1855 insertions(+) create mode 100644 isosurface.scad diff --git a/isosurface.scad b/isosurface.scad new file mode 100644 index 00000000..5f2f06de --- /dev/null +++ b/isosurface.scad @@ -0,0 +1,1855 @@ +///////////////////////////////////////////////////////////////////// +// LibFile: isosurface.scad +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), +// are bounded and closed organic surfaces that smoothly blend together. +// Metaballs are one specific kind of [isosurface](https://en.wikipedia.org/wiki/Isosurface). +// . +// An isosurface, or implicit surface, is a three-dimensional surface representing all points of a +// constant 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 shapes. For example, spherical metaballs can be formulated using a set of point +// centers that define the metaballs locations. For a single metaball, a function is defined each +// each point in a 3D volume based on the distance from that point to the metaball center. The +// combined contributions from all the metaballs results in a function that varies in a complicated +// way across the volume. When two metaballs are far apart, they appear simply as spheres, but when +// they are close together they enlarge and reach toward each other and meld together in a smooth +// fashion. The resulting metaball model appears as smoothly blended blobby shapes. The +// implementation below provides metaballs of a variety of types including spheres, cuboids and +// cylinders (cones), with optional parameters to adjust the influence of one metaball on others, +// and the cutoff distance where the metaball's influence stops. +// . +// An isosurface can be defined using any function of three variables: +// 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". +// Changing the isovalue tends to grow or shrink the isosurface, depending on how the function is +// defined. Since metaballs are isosurfaces, they also have an isovalue. The isovalue is also known +// as the "threshold". +// . +// Some isosurface functions are unbounded, extending infinitely in all directions. A familiar example may +// be a [gryoid](https://en.wikipedia.org/wiki/Gyroid), which is often used as a volume infill pattern in +// [fused filament fabrication](https://en.wikipedia.org/wiki/Fused_filament_fabrication). The gyroid +// isosurface is unbounded and periodic in all three dimensions. +// . +// This file provides modules and functions to create a [VNF](vnf.scad) using metaballs, or from general isosurfaces. +// +// Includes: +// include +// include +// FileGroup: Advanced Modeling +// FileSummary: Isosurfaces and metaballs. +////////////////////////////////////////////////////////////////////// + + +/* +Lookup Tables for Transvoxel's Modified Marching Cubes + +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, a cube with corners 2, 3, and 7 greater than the threshold isovalue would have the index 10000110, an 8-bit binary number with bits 2, 3, and 7 set to 1, corresponding to decimal index 134. After determining the cube's index value this way, the triangulation order is looked up in a table. + +Axes are + z + (top) + | y (back) + | / + |/ + +----- x (right) + +Vertex and edge layout (heavier = and # indicate closer to viewer): + + 3 +----------+ 7 +----10----+ + /: /| /: /| + / : / | 1 2 5 6 + 1 +==========+5 | +=====9====+ | + # 2+ - - - # -+ 6 # +- - 11-# -+ + # / # / 0 3 4 7 + #/ #/ #/ #/ + 0 +==========+ 4 +=====8=====+ + +z changes fastest, then y, then x. + +----------------------------------------------------------- +Addition by Alex: +Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. + + 4(back) + 3 +----------+ 7 + /: 5(top) /| + / : / | + 1 +==========+5 | <-- 3 (side) +0(side) --> # 2+ - - - # -+ 6 + # / # / + #/ 2(bot) #/ + 0 +----------+ 4 + 1(front) +*/ + +/// four indices for each face of the cube, counterclockwise looking from inside out +_MCFaceVertexIndices = [ + [], + [0,2,3,1], // left, x=0 plane + [0,1,5,4], // front, y=0 plane + [0,4,6,2], // bottom, z=0 plane + [4,5,7,6], // right, x=voxsize plane + [2,6,7,3], // back, y=voxsize plane + [1,3,7,5], // top, z=voxsize plane +]; + +/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. +function _bbox_faces(v0, voxsize, bbox) = let( + a = v0-bbox[0], + bb1 = bbox[1] - [voxsize,voxsize,voxsize], + b = v0-bb1 +) [ + if(a[0]==0) 1, + if(a[1]==0) 2, + if(a[2]==0) 3, + if(b[0]>=0) 4, + if(b[1]>=0) 5, + if(b[2]>=0) 6 +]; +/// End of bounding-box face-clipping stuff. Back to the marching cubes triangulation.... + + +/// Pair of vertex indices for each edge on the voxel +_MCEdgeVertexIndices = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [3, 7], + [2, 6] +]; + +/// 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], + [1,0,9], + [9,1,8,8,1,3], + [3,2,11], + [2,11,0,0,11,8], + [1,0,9,3,2,11], + [11,1,2,11,9,1,11,8,9], + [10,2,1], + [2,1,10,0,3,8], + [0,9,2,2,9,10], + [8,2,3,8,10,2,8,9,10], + [1,10,3,3,10,11], + [10,0,1,10,8,0,10,11,8], + [9,3,0,9,11,3,9,10,11], + [9,10,8,8,10,11], + [7,4,8], + [0,3,4,4,3,7], + [0,9,1,4,8,7], + [1,4,9,1,7,4,1,3,7], + [11,3,2,8,7,4], + [4,11,7,4,2,11,4,0,2], + [3,2,11,0,9,1,4,8,7], + [9,1,4,4,1,7,7,1,2,7,2,11], + [7,4,8,1,10,2], + [7,4,3,3,4,0,10,2,1], + [10,2,9,9,2,0,7,4,8], + [7,4,9,7,9,2,9,10,2,3,7,2], + [1,10,3,3,10,11,4,8,7], + [4,0,7,0,1,10,7,0,10,7,10,11], + [7,4,8,9,3,0,9,11,3,9,10,11], + [7,4,11,4,9,11,9,10,11], + [5,9,4], + [8,0,3,9,4,5], + [1,0,5,5,0,4], + [5,8,4,5,3,8,5,1,3], + [3,2,11,5,9,4], + [2,11,0,0,11,8,5,9,4], + [4,5,0,0,5,1,11,3,2], + [11,8,2,8,4,5,2,8,5,2,5,1], + [5,9,4,1,10,2], + [0,3,8,1,10,2,5,9,4], + [2,5,10,2,4,5,2,0,4], + [4,5,8,8,5,3,3,5,10,3,10,2], + [11,3,10,10,3,1,4,5,9], + [4,5,9,10,0,1,10,8,0,10,11,8], + [4,5,10,4,10,3,10,11,3,0,4,3], + [4,5,8,5,10,8,10,11,8], + [5,9,7,7,9,8], + [3,9,0,3,5,9,3,7,5], + [7,0,8,7,1,0,7,5,1], + [3,7,1,1,7,5], + [5,9,7,7,9,8,2,11,3], + [5,9,0,5,0,11,0,2,11,7,5,11], + [2,11,3,7,0,8,7,1,0,7,5,1], + [2,11,1,11,7,1,7,5,1], + [8,7,9,9,7,5,2,1,10], + [10,2,1,3,9,0,3,5,9,3,7,5], + [2,0,10,0,8,7,10,0,7,10,7,5], + [10,2,5,2,3,5,3,7,5], + [5,9,8,5,8,7,1,10,3,10,11,3], + [1,10,0,0,10,11,0,11,7,0,7,5,0,5,9], + [8,7,0,0,7,5,0,5,10,0,10,11,0,11,3], + [5,11,7,10,11,5], + [11,6,7], + [3,8,0,7,11,6], + [1,0,9,7,11,6], + [9,1,8,8,1,3,6,7,11], + [6,7,2,2,7,3], + [0,7,8,0,6,7,0,2,6], + [6,7,2,2,7,3,9,1,0], + [9,1,2,9,2,7,2,6,7,8,9,7], + [10,2,1,11,6,7], + [2,1,10,3,8,0,7,11,6], + [0,9,2,2,9,10,7,11,6], + [6,7,11,8,2,3,8,10,2,8,9,10], + [7,10,6,7,1,10,7,3,1], + [1,10,0,0,10,8,8,10,6,8,6,7], + [9,10,0,10,6,7,0,10,7,0,7,3], + [6,7,10,7,8,10,8,9,10], + [4,8,6,6,8,11], + [6,3,11,6,0,3,6,4,0], + [11,6,8,8,6,4,1,0,9], + [6,4,11,4,9,1,11,4,1,11,1,3], + [2,8,3,2,4,8,2,6,4], + [0,2,4,4,2,6], + [9,1,0,2,8,3,2,4,8,2,6,4], + [9,1,4,1,2,4,2,6,4], + [4,8,6,6,8,11,1,10,2], + [1,10,2,6,3,11,6,0,3,6,4,0], + [0,9,10,0,10,2,4,8,6,8,11,6], + [11,6,3,3,6,4,3,4,9,3,9,10,3,10,2], + [1,10,6,1,6,8,6,4,8,3,1,8], + [1,10,0,10,6,0,6,4,0], + [0,9,3,3,9,10,3,10,6,3,6,4,3,4,8], + [4,10,6,9,10,4], + [4,5,9,6,7,11], + [7,11,6,8,0,3,9,4,5], + [1,0,5,5,0,4,11,6,7], + [11,6,7,5,8,4,5,3,8,5,1,3], + [3,2,7,7,2,6,9,4,5], + [5,9,4,0,7,8,0,6,7,0,2,6], + [1,0,4,1,4,5,3,2,7,2,6,7], + [4,5,8,8,5,1,8,1,2,8,2,6,8,6,7], + [6,7,11,5,9,4,1,10,2], + [5,9,4,7,11,6,0,3,8,2,1,10], + [7,11,6,2,5,10,2,4,5,2,0,4], + [6,7,11,3,8,4,3,4,5,3,5,2,2,5,10], + [9,4,5,7,10,6,7,1,10,7,3,1], + [5,9,4,8,0,1,8,1,10,8,10,7,7,10,6], + [6,7,10,10,7,3,10,3,0,10,0,4,10,4,5], + [4,5,8,8,5,10,8,10,6,8,6,7], + [9,6,5,9,11,6,9,8,11], + [0,3,9,9,3,5,5,3,11,5,11,6], + [1,0,8,1,8,6,8,11,6,5,1,6], + [11,6,3,6,5,3,5,1,3], + [2,6,3,6,5,9,3,6,9,3,9,8], + [5,9,6,9,0,6,0,2,6], + [3,2,8,8,2,6,8,6,5,8,5,1,8,1,0], + [1,6,5,2,6,1], + [2,1,10,9,6,5,9,11,6,9,8,11], + [2,1,10,5,9,0,5,0,3,5,3,6,6,3,11], + [10,2,5,5,2,0,5,0,8,5,8,11,5,11,6], + [10,2,5,5,2,3,5,3,11,5,11,6], + [5,9,6,6,9,8,6,8,3,6,3,1,6,1,10], + [5,9,6,6,9,0,6,0,1,6,1,10], + [8,3,0,5,10,6], + [6,5,10], + [6,10,5], + [3,8,0,5,6,10], + [9,1,0,10,5,6], + [3,8,1,1,8,9,6,10,5], + [6,10,5,2,11,3], + [8,0,11,11,0,2,5,6,10], + [10,5,6,1,0,9,3,2,11], + [5,6,10,11,1,2,11,9,1,11,8,9], + [2,1,6,6,1,5], + [5,6,1,1,6,2,8,0,3], + [6,9,5,6,0,9,6,2,0], + [8,9,3,9,5,6,3,9,6,3,6,2], + [3,6,11,3,5,6,3,1,5], + [5,6,11,5,11,0,11,8,0,1,5,0], + [0,9,3,3,9,11,11,9,5,11,5,6], + [5,6,9,6,11,9,11,8,9], + [7,4,8,5,6,10], + [0,3,4,4,3,7,10,5,6], + [4,8,7,9,1,0,10,5,6], + [6,10,5,1,4,9,1,7,4,1,3,7], + [11,3,2,7,4,8,5,6,10], + [10,5,6,4,11,7,4,2,11,4,0,2], + [7,4,8,3,2,11,9,1,0,10,5,6], + [10,5,6,7,4,9,7,9,1,7,1,11,11,1,2], + [2,1,6,6,1,5,8,7,4], + [7,4,0,7,0,3,5,6,1,6,2,1], + [8,7,4,6,9,5,6,0,9,6,2,0], + [5,6,9,9,6,2,9,2,3,9,3,7,9,7,4], + [4,8,7,3,6,11,3,5,6,3,1,5], + [7,4,11,11,4,0,11,0,1,11,1,5,11,5,6], + [4,8,7,11,3,0,11,0,9,11,9,6,6,9,5], + [5,6,9,9,6,11,9,11,7,9,7,4], + [9,4,10,10,4,6], + [6,10,4,4,10,9,3,8,0], + [0,10,1,0,6,10,0,4,6], + [3,8,4,3,4,10,4,6,10,1,3,10], + [9,4,10,10,4,6,3,2,11], + [8,0,2,8,2,11,9,4,10,4,6,10], + [11,3,2,0,10,1,0,6,10,0,4,6], + [2,11,1,1,11,8,1,8,4,1,4,6,1,6,10], + [4,1,9,4,2,1,4,6,2], + [3,8,0,4,1,9,4,2,1,4,6,2], + [4,6,0,0,6,2], + [3,8,2,8,4,2,4,6,2], + [3,1,11,1,9,4,11,1,4,11,4,6], + [9,4,1,1,4,6,1,6,11,1,11,8,1,8,0], + [11,3,6,3,0,6,0,4,6], + [8,6,11,4,6,8], + [10,7,6,10,8,7,10,9,8], + [10,9,6,9,0,3,6,9,3,6,3,7], + [8,7,0,0,7,1,1,7,6,1,6,10], + [6,10,7,10,1,7,1,3,7], + [3,2,11,10,7,6,10,8,7,10,9,8], + [6,10,7,7,10,9,7,9,0,7,0,2,7,2,11], + [11,3,2,1,0,8,1,8,7,1,7,10,10,7,6], + [6,10,7,7,10,1,7,1,2,7,2,11], + [8,7,6,8,6,1,6,2,1,9,8,1], + [0,3,9,9,3,7,9,7,6,9,6,2,9,2,1], + [8,7,0,7,6,0,6,2,0], + [7,2,3,6,2,7], + [11,3,6,6,3,1,6,1,9,6,9,8,6,8,7], + [11,7,6,1,9,0], + [11,3,6,6,3,0,6,0,8,6,8,7], + [11,7,6], + [10,5,11,11,5,7], + [10,5,11,11,5,7,0,3,8], + [7,11,5,5,11,10,0,9,1], + [3,8,9,3,9,1,7,11,5,11,10,5], + [5,2,10,5,3,2,5,7,3], + [0,2,8,2,10,5,8,2,5,8,5,7], + [0,9,1,5,2,10,5,3,2,5,7,3], + [10,5,2,2,5,7,2,7,8,2,8,9,2,9,1], + [1,11,2,1,7,11,1,5,7], + [8,0,3,1,11,2,1,7,11,1,5,7], + [0,9,5,0,5,11,5,7,11,2,0,11], + [3,8,2,2,8,9,2,9,5,2,5,7,2,7,11], + [5,7,1,1,7,3], + [8,0,7,0,1,7,1,5,7], + [0,9,3,9,5,3,5,7,3], + [9,7,8,5,7,9], + [8,5,4,8,10,5,8,11,10], + [10,5,4,10,4,3,4,0,3,11,10,3], + [1,0,9,8,5,4,8,10,5,8,11,10], + [9,1,4,4,1,3,4,3,11,4,11,10,4,10,5], + [10,5,2,2,5,3,3,5,4,3,4,8], + [10,5,2,5,4,2,4,0,2], + [9,1,0,3,2,10,3,10,5,3,5,8,8,5,4], + [10,5,2,2,5,4,2,4,9,2,9,1], + [1,5,2,5,4,8,2,5,8,2,8,11], + [2,1,11,11,1,5,11,5,4,11,4,0,11,0,3], + [4,8,5,5,8,11,5,11,2,5,2,0,5,0,9], + [5,4,9,2,3,11], + [4,8,5,8,3,5,3,1,5], + [0,5,4,1,5,0], + [0,9,3,3,9,5,3,5,4,3,4,8], + [5,4,9], + [11,4,7,11,9,4,11,10,9], + [0,3,8,11,4,7,11,9,4,11,10,9], + [0,4,1,4,7,11,1,4,11,1,11,10], + [7,11,4,4,11,10,4,10,1,4,1,3,4,3,8], + [9,4,7,9,7,2,7,3,2,10,9,2], + [8,0,7,7,0,2,7,2,10,7,10,9,7,9,4], + [1,0,10,10,0,4,10,4,7,10,7,3,10,3,2], + [7,8,4,10,1,2], + [9,4,1,1,4,2,2,4,7,2,7,11], + [8,0,3,2,1,9,2,9,4,2,4,11,11,4,7], + [7,11,4,11,2,4,2,0,4], + [3,8,2,2,8,4,2,4,7,2,7,11], + [9,4,1,4,7,1,7,3,1], + [9,4,1,1,4,7,1,7,8,1,8,0], + [3,4,7,0,4,3], + [7,8,4], + [8,11,9,9,11,10], + [0,3,9,3,11,9,11,10,9], + [1,0,10,0,8,10,8,11,10], + [10,3,11,1,3,10], + [3,2,8,2,10,8,10,9,8], + [9,2,10,0,2,9], + [1,0,10,10,0,8,10,8,3,10,3,2], + [2,10,1], + [2,1,11,1,9,11,9,8,11], + [2,1,11,11,1,9,11,9,0,11,0,3], + [11,0,8,2,0,11], + [3,11,2], + [1,8,3,9,8,1], + [1,9,0], + [8,3,0], + [] +]; + +/// Same list as above, but with each row in reverse order. Needed for generating shells (two isosurfaces at slightly different iso values). +/// More efficient just to have a static table than to generate it each time by calling reverse() hundreds of times (although this static table was generated that way). +_MCTriangleTable_reverse = [ + [], + [0,8,3], + [9,0,1], + [3,1,8,8,1,9], + [11,2,3], + [8,11,0,0,11,2], + [11,2,3,9,0,1], + [9,8,11,1,9,11,2,1,11], + [1,2,10], + [8,3,0,10,1,2], + [10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8], + [11,10,3,3,10,1], + [8,11,10,0,8,10,1,0,10], + [11,10,9,3,11,9,0,3,9], + [11,10,8,8,10,9], + [8,4,7], + [7,3,4,4,3,0], + [7,8,4,1,9,0], + [7,3,1,4,7,1,9,4,1], + [4,7,8,2,3,11], + [2,0,4,11,2,4,7,11,4], + [7,8,4,1,9,0,11,2,3], + [11,2,7,2,1,7,7,1,4,4,1,9], + [2,10,1,8,4,7], + [1,2,10,0,4,3,3,4,7], + [8,4,7,0,2,9,9,2,10], + [2,7,3,2,10,9,2,9,7,9,4,7], + [7,8,4,11,10,3,3,10,1], + [11,10,7,10,0,7,10,1,0,7,0,4], + [11,10,9,3,11,9,0,3,9,8,4,7], + [11,10,9,11,9,4,11,4,7], + [4,9,5], + [5,4,9,3,0,8], + [4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5], + [4,9,5,11,2,3], + [4,9,5,8,11,0,0,11,2], + [2,3,11,1,5,0,0,5,4], + [1,5,2,5,8,2,5,4,8,2,8,11], + [2,10,1,4,9,5], + [4,9,5,2,10,1,8,3,0], + [4,0,2,5,4,2,10,5,2], + [2,10,3,10,5,3,3,5,8,8,5,4], + [9,5,4,1,3,10,10,3,11], + [8,11,10,0,8,10,1,0,10,9,5,4], + [3,4,0,3,11,10,3,10,4,10,5,4], + [8,11,10,8,10,5,8,5,4], + [8,9,7,7,9,5], + [5,7,3,9,5,3,0,9,3], + [1,5,7,0,1,7,8,0,7], + [5,7,1,1,7,3], + [3,11,2,8,9,7,7,9,5], + [11,5,7,11,2,0,11,0,5,0,9,5], + [1,5,7,0,1,7,8,0,7,3,11,2], + [1,5,7,1,7,11,1,11,2], + [10,1,2,5,7,9,9,7,8], + [5,7,3,9,5,3,0,9,3,1,2,10], + [5,7,10,7,0,10,7,8,0,10,0,2], + [5,7,3,5,3,2,5,2,10], + [3,11,10,3,10,1,7,8,5,8,9,5], + [9,5,0,5,7,0,7,11,0,11,10,0,0,10,1], + [3,11,0,11,10,0,10,5,0,5,7,0,0,7,8], + [5,11,10,7,11,5], + [7,6,11], + [6,11,7,0,8,3], + [6,11,7,9,0,1], + [11,7,6,3,1,8,8,1,9], + [3,7,2,2,7,6], + [6,2,0,7,6,0,8,7,0], + [0,1,9,3,7,2,2,7,6], + [7,9,8,7,6,2,7,2,9,2,1,9], + [7,6,11,1,2,10], + [6,11,7,0,8,3,10,1,2], + [6,11,7,10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8,11,7,6], + [1,3,7,10,1,7,6,10,7], + [7,6,8,6,10,8,8,10,0,0,10,1], + [3,7,0,7,10,0,7,6,10,0,10,9], + [10,9,8,10,8,7,10,7,6], + [11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6], + [9,0,1,4,6,8,8,6,11], + [3,1,11,1,4,11,1,9,4,11,4,6], + [4,6,2,8,4,2,3,8,2], + [6,2,4,4,2,0], + [4,6,2,8,4,2,3,8,2,0,1,9], + [4,6,2,4,2,1,4,1,9], + [2,10,1,11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6,2,10,1], + [6,11,8,6,8,4,2,10,0,10,9,0], + [2,10,3,10,9,3,9,4,3,4,6,3,3,6,11], + [8,1,3,8,4,6,8,6,1,6,10,1], + [0,4,6,0,6,10,0,10,1], + [8,4,3,4,6,3,6,10,3,10,9,3,3,9,0], + [4,10,9,6,10,4], + [11,7,6,9,5,4], + [5,4,9,3,0,8,6,11,7], + [7,6,11,4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5,7,6,11], + [5,4,9,6,2,7,7,2,3], + [6,2,0,7,6,0,8,7,0,4,9,5], + [7,6,2,7,2,3,5,4,1,4,0,1], + [7,6,8,6,2,8,2,1,8,1,5,8,8,5,4], + [2,10,1,4,9,5,11,7,6], + [10,1,2,8,3,0,6,11,7,4,9,5], + [4,0,2,5,4,2,10,5,2,6,11,7], + [10,5,2,2,5,3,5,4,3,4,8,3,11,7,6], + [1,3,7,10,1,7,6,10,7,5,4,9], + [6,10,7,7,10,8,10,1,8,1,0,8,4,9,5], + [5,4,10,4,0,10,0,3,10,3,7,10,10,7,6], + [7,6,8,6,10,8,10,5,8,8,5,4], + [11,8,9,6,11,9,5,6,9], + [6,11,5,11,3,5,5,3,9,9,3,0], + [6,1,5,6,11,8,6,8,1,8,0,1], + [3,1,5,3,5,6,3,6,11], + [8,9,3,9,6,3,9,5,6,3,6,2], + [6,2,0,6,0,9,6,9,5], + [0,1,8,1,5,8,5,6,8,6,2,8,8,2,3], + [1,6,2,5,6,1], + [11,8,9,6,11,9,5,6,9,10,1,2], + [11,3,6,6,3,5,3,0,5,0,9,5,10,1,2], + [6,11,5,11,8,5,8,0,5,0,2,5,5,2,10], + [6,11,5,11,3,5,3,2,5,5,2,10], + [10,1,6,1,3,6,3,8,6,8,9,6,6,9,5], + [10,1,6,1,0,6,0,9,6,6,9,5], + [6,10,5,0,3,8], + [10,5,6], + [5,10,6], + [10,6,5,0,8,3], + [6,5,10,0,1,9], + [5,10,6,9,8,1,1,8,3], + [3,11,2,5,10,6], + [10,6,5,2,0,11,11,0,8], + [11,2,3,9,0,1,6,5,10], + [9,8,11,1,9,11,2,1,11,10,6,5], + [5,1,6,6,1,2], + [3,0,8,2,6,1,1,6,5], + [0,2,6,9,0,6,5,9,6], + [2,6,3,6,9,3,6,5,9,3,9,8], + [5,1,3,6,5,3,11,6,3], + [0,5,1,0,8,11,0,11,5,11,6,5], + [6,5,11,5,9,11,11,9,3,3,9,0], + [9,8,11,9,11,6,9,6,5], + [10,6,5,8,4,7], + [6,5,10,7,3,4,4,3,0], + [6,5,10,0,1,9,7,8,4], + [7,3,1,4,7,1,9,4,1,5,10,6], + [10,6,5,8,4,7,2,3,11], + [2,0,4,11,2,4,7,11,4,6,5,10], + [6,5,10,0,1,9,11,2,3,8,4,7], + [2,1,11,11,1,7,1,9,7,9,4,7,6,5,10], + [4,7,8,5,1,6,6,1,2], + [1,2,6,1,6,5,3,0,7,0,4,7], + [0,2,6,9,0,6,5,9,6,4,7,8], + [4,7,9,7,3,9,3,2,9,2,6,9,9,6,5], + [5,1,3,6,5,3,11,6,3,7,8,4], + [6,5,11,5,1,11,1,0,11,0,4,11,11,4,7], + [5,9,6,6,9,11,9,0,11,0,3,11,7,8,4], + [4,7,9,7,11,9,11,6,9,9,6,5], + [6,4,10,10,4,9], + [0,8,3,9,10,4,4,10,6], + [6,4,0,10,6,0,1,10,0], + [10,3,1,10,6,4,10,4,3,4,8,3], + [11,2,3,6,4,10,10,4,9], + [10,6,4,10,4,9,11,2,8,2,0,8], + [6,4,0,10,6,0,1,10,0,2,3,11], + [10,6,1,6,4,1,4,8,1,8,11,1,1,11,2], + [2,6,4,1,2,4,9,1,4], + [2,6,4,1,2,4,9,1,4,0,8,3], + [2,6,0,0,6,4], + [2,6,4,2,4,8,2,8,3], + [6,4,11,4,1,11,4,9,1,11,1,3], + [0,8,1,8,11,1,11,6,1,6,4,1,1,4,9], + [6,4,0,6,0,3,6,3,11], + [8,6,4,11,6,8], + [8,9,10,7,8,10,6,7,10], + [7,3,6,3,9,6,3,0,9,6,9,10], + [10,6,1,6,7,1,1,7,0,0,7,8], + [7,3,1,7,1,10,7,10,6], + [8,9,10,7,8,10,6,7,10,11,2,3], + [11,2,7,2,0,7,0,9,7,9,10,7,7,10,6], + [6,7,10,10,7,1,7,8,1,8,0,1,2,3,11], + [11,2,7,2,1,7,1,10,7,7,10,6], + [1,8,9,1,2,6,1,6,8,6,7,8], + [1,2,9,2,6,9,6,7,9,7,3,9,9,3,0], + [0,2,6,0,6,7,0,7,8], + [7,2,6,3,2,7], + [7,8,6,8,9,6,9,1,6,1,3,6,6,3,11], + [0,9,1,6,7,11], + [7,8,6,8,0,6,0,3,6,6,3,11], + [6,7,11], + [7,5,11,11,5,10], + [8,3,0,7,5,11,11,5,10], + [1,9,0,10,11,5,5,11,7], + [5,10,11,5,11,7,1,9,3,9,8,3], + [3,7,5,2,3,5,10,2,5], + [7,5,8,5,2,8,5,10,2,8,2,0], + [3,7,5,2,3,5,10,2,5,1,9,0], + [1,9,2,9,8,2,8,7,2,7,5,2,2,5,10], + [7,5,1,11,7,1,2,11,1], + [7,5,1,11,7,1,2,11,1,3,0,8], + [11,0,2,11,7,5,11,5,0,5,9,0], + [11,7,2,7,5,2,5,9,2,9,8,2,2,8,3], + [3,7,1,1,7,5], + [7,5,1,7,1,0,7,0,8], + [3,7,5,3,5,9,3,9,0], + [9,7,5,8,7,9], + [10,11,8,5,10,8,4,5,8], + [3,10,11,3,0,4,3,4,10,4,5,10], + [10,11,8,5,10,8,4,5,8,9,0,1], + [5,10,4,10,11,4,11,3,4,3,1,4,4,1,9], + [8,4,3,4,5,3,3,5,2,2,5,10], + [2,0,4,2,4,5,2,5,10], + [4,5,8,8,5,3,5,10,3,10,2,3,0,1,9], + [1,9,2,9,4,2,4,5,2,2,5,10], + [11,8,2,8,5,2,8,4,5,2,5,1], + [3,0,11,0,4,11,4,5,11,5,1,11,11,1,2], + [9,0,5,0,2,5,2,11,5,11,8,5,5,8,4], + [11,3,2,9,4,5], + [5,1,3,5,3,8,5,8,4], + [0,5,1,4,5,0], + [8,4,3,4,5,3,5,9,3,3,9,0], + [9,4,5], + [9,10,11,4,9,11,7,4,11], + [9,10,11,4,9,11,7,4,11,8,3,0], + [10,11,1,11,4,1,11,7,4,1,4,0], + [8,3,4,3,1,4,1,10,4,10,11,4,4,11,7], + [2,9,10,2,3,7,2,7,9,7,4,9], + [4,9,7,9,10,7,10,2,7,2,0,7,7,0,8], + [2,3,10,3,7,10,7,4,10,4,0,10,10,0,1], + [2,1,10,4,8,7], + [11,7,2,7,4,2,2,4,1,1,4,9], + [7,4,11,11,4,2,4,9,2,9,1,2,3,0,8], + [4,0,2,4,2,11,4,11,7], + [11,7,2,7,4,2,4,8,2,2,8,3], + [1,3,7,1,7,4,1,4,9], + [0,8,1,8,7,1,7,4,1,1,4,9], + [3,4,0,7,4,3], + [4,8,7], + [10,11,9,9,11,8], + [9,10,11,9,11,3,9,3,0], + [10,11,8,10,8,0,10,0,1], + [10,3,1,11,3,10], + [8,9,10,8,10,2,8,2,3], + [9,2,0,10,2,9], + [2,3,10,3,8,10,8,0,10,10,0,1], + [1,10,2], + [11,8,9,11,9,1,11,1,2], + [3,0,11,0,9,11,9,1,11,11,1,2], + [11,0,2,8,0,11], + [2,11,3], + [1,8,9,3,8,1], + [0,9,1], + [0,3,8], + [] +]; + + +/// _cubindex() - private function, called by _isosurface_cubes() +/// Return the index ID of a voxel depending on the field strength at each corner exceeding isoval. +function _cubeindex(f, isoval) = + (f[0] > isoval ? 1 : 0) + + (f[1] > isoval ? 2 : 0) + + (f[2] > isoval ? 4 : 0) + + (f[3] > isoval ? 8 : 0) + + (f[4] > isoval ? 16 : 0) + + (f[5] > isoval ? 32 : 0) + + (f[6] > isoval ? 64 : 0) + + (f[7] > isoval ? 128 : 0); + + +/// isosurface_cubes() - private function, called by isosurface() +/// This implements a marching cube algorithm, sacrificing some memory in favor of speed. +/// Return a list of voxel cube structures that have one or both surfaces isovalmin or isovalmax intersecting them, and cubes inside the isosurface volume that are at the bounds of the bounding box. +/// The cube structure is: +/// [cubecoord, cubeindex_isomin, cubeindex_isomax, field, bfaces] +/// where +/// cubecoord is the [x,y,z] coordinate of the front left bottom corner of the voxel, +/// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections +/// cf is vector containing the 6 field strength values at each corner of the voxel cube +/// 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, isovalmin, isovalmax, closed=true) = let( + // get field intensities + field = is_def(fieldarray) + ? fieldarray + : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+[hv,hv,hv]) [ + for(x=[v.x:voxsize:b1.x]) [ + for(y=[v.y:voxsize:b1.y]) [ + for(z=[v.z:voxsize:b1.z]) + fieldfunc(x,y,z) + ] + ] + ], + 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) + for(j=[0:ny]) let(y=v0[1]+voxsize*j) + for(k=[0:nz]) let(z=v0[2]+voxsize*k) + let(i1=i+1, j1=j+1, k1=k+1, + cf = [ // cube corner field values + min(1e9,max(-1e9,field[i][j][k])), + min(1e9,max(-1e9,field[i][j][k1])), + min(1e9,max(-1e9,field[i][j1][k])), + min(1e9,max(-1e9,field[i][j1][k1])), + min(1e9,max(-1e9,field[i1][j][k])), + min(1e9,max(-1e9,field[i1][j][k1])), + min(1e9,max(-1e9,field[i1][j1][k])), + min(1e9,max(-1e9,field[i1][j1][k1])) + ], + mincf = min(cf), + maxcf = max(cf), + cubecoord = [x,y,z], + bfaces = closed ? _bbox_faces(cubecoord, voxsize, bbox) : [], + cubefound_isomin = (mincf<=isovalmin && isovalmin0) for(ei=epathmin) // min surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(lenmax>0) for(ei=epathmax) // max surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(n_outer>0) for(bf = bbfaces) + each _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) + ] +]; + + +/// Generate triangles for the special case of voxel faces clipped by the bounding box +function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( + vi = _MCFaceVertexIndices[bbface], // four voxel face vertex indices + //vfc = [ for(i=vi) vcube[i] ], // four voxel face vertex coordinates + //fld = [ for(i=vi) f[i] ], // four corner field values + pgon = flatten([ + for(i=[0:3]) let( // for each line segment... + vi0=vi[i], // voxel corner 0 index + vi1=vi[(i+1)%4], // voxel corner 1 index + f0 = f[vi0], // field value at corner 0 + f1 = f[vi1], // field value at corner 1 + fmin = min(f0, f1), // min field of the corners + fmax = max(f0, f1), // max field of the corners + ilowbetween = (fmin < isovalmin && isovalmin < fmax), + ihighbetween = (fmin < isovalmax && isovalmax < fmax), + denom = f1-f0 + ) [ // traverse the edge, output vertices as they are found + if(isovalmin <= f0 && f0 <= isovalmax)// && abs(f1-f0)>0.001) + // vertex 0 is on or between min and max isovalues + //echo(vfc, fld) + vcube[vi0], + //else if (abs(isovalmin-f0)<0.00001 || abs(isovalmax-f0)<0.00001) + // 0.5*(vcube[vi0]+vcube[vi1]), + // for f0f1) + let(u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f0)/denom) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(ilowbetween && f0>f1) + let(u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f0)/denom) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + ] + ]), + npgon = len(pgon), + triangles = npgon<3 ? [] : [ + for(i=[1:len(pgon)-2]) [pgon[0], pgon[i], pgon[i+1]] + ]) flatten(triangles); + + +/// _showstats() (Private function) - called by isosurface() and metaballs() +/// Display statistics about isosurface +function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( + v = column(cubes, 0), // extract cube vertices + x = column(v,0), // extract x values + y = column(v,1), // extract y values + z = column(v,2), // extract z values + xmin = min(x), + xmax = max(x)+voxelsize, + ymin = min(y), + ymax = max(y)+voxelsize, + zmin = min(z), + zmax = max(z)+voxelsize, + ntri = len(faces), + nvox = len(cubes) +) echo(str("\nIsosurface statistics:\n Outer isovalue = ", isoval, "\n Voxel size = ", voxelsize, + "\n Voxels found containing surface = ", nvox, "\n Triangles = ", ntri, + "\n Voxel bounding box for all data = ", bbox, + "\n Voxel bounding box for isosurface = ", [[xmin,ymin,zmin], [xmax,ymax,zmax]], + "\n")) 0; + + +/// ---------- metaball stuff starts here ---------- + +/// Animated metaball demo made with BOSL2 here: https://imgur.com/a/m29q8Qd + +/// Built-in metaball functions corresponding to each MB_ index. +/// For speed, they are split into four functions, each handling a different combination of influence != 1 or influence == 1, and cutoff < INF or cutoff == INF. + +/// public metaball cutoff function if anyone wants it (demonstrated in example) + +function mb_cutoff(dist, cutoff) = dist>=cutoff ? 0 : 0.5*(cos(180*(dist/cutoff)^4)+1); + + +/// metaball sphere + +function _mb_sphere_basic(dv, r, neg) = neg*r/norm(dv); +function _mb_sphere_influence(dv, r, ex, neg) = neg * (r/norm(dv))^ex; +function _mb_sphere_cutoff(dv, r, cutoff, neg) = let(dist=norm(dv)) + neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_sphere_full(dv, r, cutoff, ex, neg) = let(dist=norm(dv)) + neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_sphere_basic(dv,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_sphere_influence(dv,r,1/influence, neg) + : influence==1 ? function(dv) _mb_sphere_cutoff(dv,r,cutoff,neg) + : function(dv) _mb_sphere_full(dv,r,cutoff,1/influence,neg); + + +/// metaball rounded cube + +function _mb_cuboid_basic(dv, inv_size, xp, neg) = + let( + dv=inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) + ) neg/dist; +function _mb_cuboid_influence(dv, inv_size, xp, ex, neg) = let( + dv=inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg / dist^ex; +function _mb_cuboid_cutoff(dv, inv_size, xp, cutoff, neg) = let( + dv = inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist; +function _mb_cuboid_full(dv, inv_size, xp, ex, cutoff, neg) = let( + dv = inv_size * dv, + dist = xp >= 1100 ? max(v_abs(dv)) + :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist^ex; + +function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + assert((is_finite(size) && size>0) || (is_vector(size) && all_positive(size)), "\nsize must be a positive number or a 3-vector of positive values.") + let( + xp = _squircle_se_exponent(squareness), + neg = negative ? -1 : 1, + inv_size = is_num(size) ? 2/size + : [[2/size.x,0,0],[0,2/size.y,0],[0,0,2/size.z]] + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, inv_size, xp, neg) + : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, inv_size, xp, 1/influence, neg) + : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, inv_size, xp, cutoff, neg) + : function (dv) _mb_cuboid_full(dv, inv_size, xp, 1/influence, cutoff, neg); + + +/// metaball rounded cylinder / cone + +function _revsurf_basic(dv, path, coef, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] ? coef*(1+dist) : coef/(1+dist)); + +function _revsurf_influence(dv, path, coef, exp, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] ? (coef*(1+dist))^exp : (coef/(1+dist))^exp); + +function _revsurf_cutoff(dv, path, coef, cutoff, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ) + neg * (inside_check==[] + ? (coef*(1+dist)) : mb_cutoff(dist-coef, cutoff) * (coef/(1+dist)) ); + +function _revsurf_full(dv, path, coef, cutoff, exp, neg) = + let( + pt = [norm([dv.x,dv.y]), dv.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside_check = [ + for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1 + ] + ) + neg * (inside_check==[] + ? (coef*(1+dist))^exp : mb_cutoff(dist-coef, cutoff) * (coef/(1+dist))^exp ); + +function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influence=1, negative=false) = + let( + r1 = get_radius(r1=r1,r=r, d1=d1, d=d), + r2 = get_radius(r1=r2,r=r, d1=d2, d=d), + h = first_defined([h,l,height,length],"h,l,height,length") + ) + assert(is_finite(rounding) && rounding>=0, "rounding must be a nonnegative number") + assert(is_finite(r1) && r1>0, "r/r1/d/d1 must be a positive number") + assert(is_finite(r2) && r2>0, "r/r2/d/d2 must be a positive number") + let( + vang = atan2(r1-r2,h), + facelen = adj_ang_to_hyp(h, abs(vang)), + roundlen1 = rounding/tan(45-vang/2), + roundlen2 = rounding/tan(45+vang/2), + sides = [[0,h/2], [r2,h/2], [r1,-h/2], [0,-h/2]], + neg = negative ? -1 : 1 + ) + assert(roundlen1 <= r1, "size of rounding is larger than the r1 radius of the cylinder/cone") + assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") + assert(roundlen1+roundlen2 < facelen, "Roundings don't fit on the edge length of the cylinder/cone") + let(shifted = offset(sides, delta=-rounding, closed=false)) + !is_finite(cutoff) && influence==1 ? function(dv) _revsurf_basic(dv, shifted, 1+rounding, neg) + : !is_finite(cutoff) ? function(dv) _revsurf_influence(dv, shifted, 1+rounding, 1/influence, neg) + : influence==1 ? function(dv) _revsurf_cutoff(dv, shifted, 1+rounding, cutoff, neg) + : function (dv) _revsurf_full(dv, shifted, 1+rounding, cutoff, 1/influence, neg); + +// metaball capsule (round-ended cylinder) + +function _mb_capsule_basic(dv, hl, r, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +) neg*r/dist; +function _mb_capsule_influence(dv, hl, r, ex, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +) neg * (r/dist)^ex; +function _mb_capsule_cutoff(dv, hl, r, cutoff, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + r = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + sh = h-2*r, // straight side length + dum3 = assert(sh>0, "\nTotal length must accommodate rounded ends of cylinder."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_capsule_basic(dv,sh/2,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_capsule_influence(dv,sh/2,r,1/influence, neg) + : influence==1 ? function(dv) _mb_capsule_cutoff(dv,sh/2,r,cutoff,neg) + : function (dv) _mb_capsule_full(dv, sh/2, r, cutoff, 1/influence, neg); + + +/// metaball disk with rounded edge + +function _mb_disk_basic(dv, hl, r, neg) = + let( + rdist=norm([dv.x,dv.y]), + dist = rdist0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + h2 = h/2, + or = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && or>0, "\ninvalid radius or diameter."), + r = or - h2, + dum3 = assert(r>0, "\nDiameter must be greater than height."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_disk_basic(dv,h2,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_disk_influence(dv,h2,r,1/influence, neg) + : influence==1 ? function(dv) _mb_disk_cutoff(dv,h2,r,cutoff,neg) + : function (dv) _mb_disk_full(dv, h2, r, cutoff, 1/influence, neg); + + +/// metaball connector cylinder - calls mb_capsule* functions after transform + +function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + dum1 = assert(is_vector(p1,3), "\nConnector start point p1 must be a 3D coordinate.") + assert(is_vector(p2,3), "\nConnector end point p2 must be a 3D coordinate.") + assert(p1 != p2, "\nStart and end points p1 and p2 cannot be the same."), + r = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1, + dc = p2-p1, // center-to-center distance + midpt = reverse(-0.5*(p1+p2)), + h = norm(dc)/2, // center-to-center length (cylinder height) + transform = submatrix(down(h)*rot(from=dc,to=UP)*move(-p1) ,[0:2], [0:3]) + ) + !is_finite(cutoff) && influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_basic(newdv,h,r,neg) + : !is_finite(cutoff) ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_influence(newdv,h,r,1/influence, neg) + : influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_cutoff(newdv,h,r,cutoff,neg) + : function (dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_full(newdv, h, r, cutoff, 1/influence, neg); + + +/// metaball octahedron + +function _mb_octahedron_basic(dv, r, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg*r/dist; +function _mb_octahedron_influence(dv, r, ex, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * (r/dist)^ex; +function _mb_octahedron_cutoff(dv, r, cutoff, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_octahedron_full(dv, r, cutoff, ex, neg) = + let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && is_num(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_octahedron_basic(dv,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_octahedron_influence(dv,r,1/influence, neg) + : influence==1 ? function(dv) _mb_octahedron_cutoff(dv,r,cutoff,neg) + : function(dv) _mb_octahedron_full(dv,r,cutoff,1/influence,neg); + + +/// metaball torus + +function _mb_torus_basic(dv, rmaj, rmin, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg*rmin/dist; +function _mb_torus_influence(dv, rmaj, rmin, ex, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * (rmin/dist)^ex; +function _mb_torus_cutoff(dv, rmaj, rmin, cutoff, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) + neg * mb_cutoff(dist, cutoff) * rmin/dist; +function _mb_torus_full(dv, rmaj, rmin, cutoff, ex, neg) = + let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) + neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; + +function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min, or,od,ir,id) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + _or = get_radius(r=or, d=od, dflt=undef), + _ir = get_radius(r=ir, d=id, dflt=undef), + _r_maj = get_radius(r=r_maj, d=d_maj, dflt=undef), + _r_min = get_radius(r=r_min, d=d_min, dflt=undef), + r_maj = is_finite(_r_maj)? _r_maj : + is_finite(_ir) && is_finite(_or)? (_or + _ir)/2 : + is_finite(_ir) && is_finite(_r_min)? (_ir + _r_min) : + is_finite(_or) && is_finite(_r_min)? (_or - _r_min) : + assert(false, "Bad major size parameter."), + r_min = is_finite(_r_min)? _r_min : + is_finite(_ir)? (maj_rad - _ir) : + is_finite(_or)? (_or - maj_rad) : + assert(false, "\nBad minor size parameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_torus_basic(dv, r_maj, r_min, neg) + : !is_finite(cutoff) ? function(dv) _mb_torus_influence(dv, r_maj, r_min, 1/influence, neg) + : influence==1 ? function(dv) _mb_torus_cutoff(dv, r_maj, r_min, cutoff, neg) + : function(dv) _mb_torus_full(dv, r_maj, r_min, cutoff, 1/influence, neg); + + +// Function&Module: metaballs() +// Synopsis: Creates a group of 3D metaballs (smoothly connected blobs). +// SynTags: Geom,VNF +// Topics: Metaballs, Isosurfaces, VNF Generators +// See Also: isosurface() +// Usage: As a module +// metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [convexity=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [convexity=], [show_stats=]); +// Description: +// ![Metaball animation](https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/images/metaball_demo.gif) +// . +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", +// can produce smoothly varying blobs and organic forms. You create metaballs by placing metaball +// objects at different locations. These objects have a basic size and shape when placed in +// isolation, but if another metaball object is nearby, the two objects interact, growing larger +// and melding together. The closer the objects are, the more they blend and meld. +// . +// The simplest metaball specification is a 1D list of alternating transformation matrices and +// metaball functions: `[trans0, func0, trans1, func1, ... ]`. Each transformation matrix +// you supply can be constructed using the usual transformation commands such as {{up()}}, +// {{right()}}, {{back()}}, {{move()}}, {{scale()}}, {{rot()}} and so on. You can multiply +// the transformations together, similar to how the transformations can be applied +// to regular objects in OpenSCAD. For example, to transform an object in regular OpenSCAD you might +// write `up(5) xrot(25) zrot(45) scale(4)`. You would provide that transformation +// as the transformation matrix `up(5) * xrot(25) * zrot(45) * scale(4)`. You can use +// scaling to produce an ellipse from a sphere, and you can even use {{skew()}} if desired. +// When no transformation is needed, give `IDENT` as the transformation. +// . +// The metaballs are evaluated over a bounding box defined by its minimum and maximum corners, +// `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. The contributions from **all** metaballs, even those outside +// the bounds, are evaluated over the bounding box. This bounding box is divided into voxels of the +// specified `voxel_size`. Smaller voxels produce a finer, smoother result at the expense of +// execution time. If the voxel size doesn't exactly divide your specified bounding box, then +// the bounding box is enlarged to contain whole voxels, and centered on your requested box. If +// the bounding box clips a metaball and `closed=true` (the default), the object is closed at the +// intersection surface. Setting `closed=false` causes the [VNF](vnf.scad) to end at the bounding box, +// resulting in a non-manifold shape with holes, exposing the inside of the object. +// . +// For metaballs with flat surfaces (the ends of `mb_cyl()`, and `mb_cuboid()` with `squareness=1`), +// avoid letting any side of the bounding box coincide with one of these flat surfaces, otherwise +// unpredictable triangulation around the edge may result. This practice would increase run-time also. +// . +// You can create metaballs in a variety of standard shapes using the predefined functions +// listed below. If you wish, you can also create custom metaball shapes using your own functions +// (see Example 19). Three parameters are available on all of the built-in metaballs to control the +// interaction of the metaballs with each other: `cutoff`, `influence`, and `negative`. +// . +// The `cutoff` parameter specifies the distance beyond which the metaball has no interaction +// with other balls. When you apply `cutoff`, a smooth suppression factor begins +// decreasing the interaction strength at half the cutoff distance and reduces the interaction to +// zero at the cutoff. Note that the smooth decrease may cause the interaction to be negligible +// closer than the cutoff distance distance, depending on the voxel size and `influence` of the +// ball. Also, depending on the value of `influence`, a cutoff that ends in the middle of +// another ball can result in strange shapes, as shown in Example 16, with the metaball +// interacting one one side of the boundary and not interacting on the other side. If you scale +// a ball, the cutoff values is also scaled. The exact way that cutoff is defined +// geometrically varies for different ball types; see below for details. +// . +// The `influence` parameter adjusts the strength of the interaction metaball objects have with each +// other. If you increase `influence` from its default of 1, the metaball interacts with other +// metaballs at a longer range, and surrounding balls grow bigger. The metaball with larger +// influence can also grow bigger because it couples more strongly with other nearby balls, but it +// can also remain nearly unchanged while influencing others when `isovalue` is greater than 1. +// Decreasing influence has the reverse effect. Small changes in influence can have a large +// effect. For example, setting `influence=2` dramatically increases the interactions at longer +// distances, and you may want to set the `cutoff` argument to limit the range influence. +// . +// The `negative` parameter, if set to `true`, creates a negative metaball, which can create +// hollows or dents in other metaballs, or swallow other metaballs entirely, making them disappear. +// Negative metaballs are always below the isovalue, so they are never directly visible; +// only their effects are visible. See Examples 15 and 16. +// . +// The `isovalue` parameter defaults to 1. If you increase it, all the objects in your model +// shrink, causing some melded objects to separate. If you decrease it, each metaball grows and +// melds more with others. Be aware that changing the isovalue affects **all** the metaballs and +// changes the entire model, possibly dramatically. +// . +// For complicated metaball assemblies you may wish to repeat a structure in different locations or +// otherwise transformed. Nested metaball specifications are supported: +// Instead of specifying a transform and function, you specify a transform and then another metaball +// specification. For example, you could set `finger=[t0,f0,t1,f1,t2,f2]` and then set +// `hand=[u0,finger,u1,finger,...]` and then invoke `metaballs()` with `[s0, hand]`. +// In effect, any metaball specification array can be treated as a single metaball. +// This is a powerful technique that lets you make groups of metaballs that you can use as individual +// metaballs in other groups, and can make your code compact and simpler to understand. See Example 21. +// . +// ***Built-in metaball functions*** +// . +// Several metaballs are defined for you to use in your models. +// All of the built-in metaballs take positional and named parameters that specify the size of the +// metaball (e.g. radius, height). The size arguments are the same as those for the regular objects +// of the same type (e.g. a sphere accepts both `r` for radius and the named parameter `d=` for +// diameter). The size parameters always specify the size of the metaball **in isolation** with +// `isovalue=1`. The metaballs can grow much bigger than their specified sizes when they interact +// with each other. The metaballs also grow bigger than their specified sizes, even in isolation, +// if `isovalue<1` and smaller than their specified sizes if `isovalue>1`. +// . +// All of the built-in functions all accept these named arguments, which are not repeated in the list below: +// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center unless otherwise noted below. Default: INF +// * `influence` — a positive number specifying the strength of interaction this ball has with other balls. Default: 1 +// * `negative` — when true, create a negative metaball. Default: false +// . +// The built in metaballs functions are listed below. As usual, arguments without a trailing `=` can be used positionally; arguments with a trailing `=` must be used as named arguments. +// The list of examples below illustrates each type of metaball interacting with another of the same type. +// . +// * `mb_sphere(r|d=)` — spherical metaball, with radius r or diameter d. You can create an ellipsoid using `scale()` as the last transformation entry of the metaball `spec` array. +// * `mb_cuboid(size, [squareness=])` — cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` is a scalar that specifies the width of the cuboid shape between the face centers. Except when `squareness=1`, the faces are always a little bit curved. +// * `mb_cyl(h|l|height|length, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=])` — vertical cylinder or cone metaball with the same dimenional arguments as {{cyl()}}. At least one of the radius or diameter arguments is required. The `rounding` argument defaults to 0 (sharp edge) if not specified. Only one rounding value is allowed: the rounding is the same at both ends. For a fully rounded cylindrical shape, consider `mb_capsule()` or `mb_disk()`, which are less flexible but have faster execution times. For this metaball, the cutoff is measured from surface of the cone with the specified dimensions. +// * `mb_disk(h|l|height|length, r|d=)` — rounded disk with flat ends. The diameter specifies the total diameter of the shape including the rounded sides and must be greater than its height. +// * `mb_capsule(h|l|height|length, r|d=)` — cylinder of radius `r` or diameter `d` with hemispherical caps. The height or length specifies the **total** height including the rounded ends. +// * `mb_connector(p1, p2, r|d=)` — a connecting rod of radius `r` or diameter `d` with hemispherical caps (like `mb_capsule()`), but specified to connect point `p1` to point `p2` (where `p1` and `p2` must be different 3D vectors). The specified points are at the centers of the two capping hemispheres. You may want to set `influence` quite low; the connectors themselves are still influenced by other metaballs, but it may be undesirable to have them influence others, or each other. If two connectors are connected, the joint may appear large and swollen unless `influence` is reduced. +// * `mb_torus([r_maj|d_maj=], [r_min|d_min=], [or=|od=], [ir=|id=])` — torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. Both major and minor radius/diameter must be specified regardless of how they are named. +// *`mb_octahedron(r|d=])` — octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip. The vertex parameter specifies the distance between the two opposite tips. +// . +// ***Metaball functions and user defined functions*** +// . +// Each metaball function is defined as a function of a 3-vector that gives the value of the metaball function +// for that point in space. As is common in metaball implementations, we define the built-in metaballs using an +// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance from the +// metaball center. The spherical metaball therefore has a simple basic definition as `f(v) = 1/norm(v)`. +// Note that with this framework, `f(v) >= c` defines a bounded object. Increasing the isovalue shrinks the +// object, and decreasing the isovalue grows the object. +// . +// In order to adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` +// then the decay becomes $1/d^{1/a}$. This means, for example, that if you set influence to 0.5 you get a +// $1/d^2$ falloff. Changing this exponent changes how the balls interact. +// . +// You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// that takes a single argument (a 3-vector) and returns a single numerical value. +// The returned value should define a function where in isovalue range [c,INF] defines a bounded object. See Example 19. +// . +// ***Voxel size and bounding box*** +// . +// The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. +// A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the +// calculation and storage of 8,000,000 function values, and more processing and memory to generate +// the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box +// requires only 8,000 function values and a modest computation time. A good rule is to keep the +// number of voxels below 10,000 for preview, and adjust the voxel size smaller for final +// rendering. A bounding box that is larger than your isosurface wastes time computing function +// values that are not needed. If the metaballs fit completely within the bounding box, you can +// call {{pointlist_bounds()}} on `vnf[0]` returned from the `metaballs()` function to get an +// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the +// same run time but increase the resolution. You can also set the parameter `show_stats=true` to +// get the bounds of the voxels containing the generated surfaces. +// . +// The point list in the returned 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 (often +// resulting from clipping by the bounding box) 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 computationall expensive +// and are not normally necessary. +// Arguments: +// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification. See above for more details, and see Example 21 for a demonstration. +// voxel_size = scalar size of the voxel cube that is used to sample the surface. +// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. +// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 +// --- +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. 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 small speed penalty. Default: false +// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(3D,NoAxes): Two spheres interacting. +// spec = [ +// left(9), mb_sphere(5), +// right(9), mb_sphere(5) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-16,-7,-7], [16,7,7]]); +// Example(3D,NoAxes): Two rounded cuboids interacting. +// spec = [ +// move([-8,-5,-5]), mb_cuboid(10), +// move([8,5,5]), mb_cuboid(10) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-15,-12,-12], [15,12,12]]); +// Example(3D,NoAxes): Two rounded `mb_cyl()` cones interacting. +// spec = [ +// left(10), mb_cyl(15, r1=8, r2=5, rounding=3), +// right(10), mb_cyl(15, r1=8, r2=5, rounding=3) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-19,-9,-10], [19,9,10]]); +// Example(3D,NoAxes): Two disks interacting. +// metaballs([ +// move([-10,0,2]), mb_disk(5,9), +// move([10,0,-2]), mb_disk(5,9) +// ], 0.5, [[-20,-10,-6], [20,10,6]]); +// Example(3D,NoAxes): Two capsules interacting. +// metaballs([ +// move([-8,0,4])*yrot(90), mb_capsule(16,3), +// move([8,0,-4])*yrot(90), mb_capsule(16,3) +// ], 0.5, [[-17,-5,-8], [17,5,8]]); +// Example(3D,NoAxes): A sphere with two connectors. +// spec = [ +// left(20), mb_sphere(6), +// IDENT, mb_connector([-20,0,0], [0,0,10], 2, influence=0.5), +// IDENT, mb_connector([0,0,10], [0,-10,0], 2, influence=0.5) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-27,-13,-7], [4,7,14]]); +// Example(3D,NoAxes): Interaction between two tori in different orientations. +// spec = [ +// move([-10,0,17]), mb_torus(r_maj=6, r_min=2), +// move([7,6,21])*xrot(90), mb_torus(r_maj=7, r_min=3) +// ]; +// voxelsize = 0.5; +// boundingbox = [[-19,-9,9], [18,10,32]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,NoAxes,VPR=[75,0,20]): Two octahedrons interacting. +// metaballs([ +// move([-10,0,3]), mb_octahedron(8), +// move([10,0,-3]), mb_octahedron(8) +// ], 0.5, [[-21,-11,-13], [21,11,13]]); +// Example(3D,VPD=110): These next five examples demonstrate the differnent types of metaball interactions. We start with two spheres 30 units apart. Each would have a radius of 10 in isolation, but because they are influencing their surroundings, each sphere mutually contributes to the size of the other. The sum of contributions between the spheres add up so that a surface plotted around the region exceeding the threshold defined by `isovalue=1` looks like a peanut shape surrounding the two spheres. +// spec = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (which is the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself. +// spec = [ +// left(15), mb_sphere(10, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,VPD=110): Here, the left sphere has less influence in addition to a cutoff. Setting `influence=0.5` results in a steeper falloff of contribution from the left sphere. Each sphere has a different size and shape due to unequal contributions based on distance. +// spec = [ +// left(15), mb_sphere(10, influence=0.5, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,VPD=110): In this example, we have two size-10 spheres as before and one tiny sphere of 1.5 units radius offset a bit on the y axis. With an isovalue of 1, this figure would appear similar to Example 9 above, but here the isovalue has been set to 2, causing the surface to shrink around a smaller volume values greater than 2. Remember, higher isovalue thresholds cause metaballs to shrink. +// spec = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10), +// fwd(15), mb_sphere(1.5) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, voxelsize, boundingbox, +// isovalue=2); +// Example(3D,VPD=110): Keeping `isovalue=2`, the influence of the tiny sphere has been set quite high, to 10. Notice that the tiny sphere shrinks a bit, but it has dramatically increased its contribution to its surroundings, causing the two other spheres to grow and meld into each other. The `influence` argument on a small metaball affects its surroundings more than itself. +// spec = [ +// move([-15,0,0]), mb_sphere(10), +// move([15,0,0]), mb_sphere(10), +// move([0,-15,0]), mb_sphere(1.5, influence=10) +// ]; +// voxelsize = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, voxelsize, boundingbox, +// isovalue=2); +// Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. +// spec = [ // spheres of different sizes +// move([-20,-20,-20]), mb_sphere(5), +// move([0,-20,-20]), mb_sphere(4), +// IDENT, mb_sphere(3), +// move([0,0,20]), mb_sphere(5), +// move([20,20,10]), mb_sphere(7) +// ]; +// voxelsize = 1.5; +// boundingbox = [[-30,-31,-31], [32,31,31]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,NoAxes): A metaball can be negative. 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]]; +// spec = [ +// move(centers[0]), mb_sphere(8), +// move(centers[1]), mb_sphere(3, negative=true) +// ]; +// voxelsize = 0.25; +// isovalue = 1; +// boundingbox = [[-7,-6,-6], [3,6,6]]; +// #metaballs(spec, voxelsize, boundingbox, isovalue); +// color("green") move_copies(centers) sphere(d=1, $fn=16); +// Example(3D,VPD=105,VPT=[3,5,4.7]): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball as high influence and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin. +// spec = [ +// back(10), mb_sphere(20), +// IDENT, mb_sphere(2, influence = 30, cutoff = 20, negative = true), +// ]; +// voxelsize = 0.5; +// boundingbox = [[-20,-4,-20], [20,30,20]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. +// spec = [ +// move([-7,-3,27])*zrot(55), mb_cuboid(6, squareness=1), +// move([5,5,21]), mb_cuboid(5), +// move([10,0,10]), mb_octahedron(5) +// ]; +// voxelsize = 0.5; // a bit slow at this resolution +// boundingbox = [[-12,-9,3], [18,10,32]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,NoAxes,VPD=205,Med): A toy airplane, constructed only from metaball spheres with scaling. The bounding box is used to clip the wingtips, tail, and belly of the fuselage. +// bounding_box = [[-55,-50,-5],[35,50,17]]; +// spec = [ +// move([-20,0,0])*scale([25,4,4]), mb_sphere(1), // fuselage +// move([30,0,5])*scale([4,0.5,8]), mb_sphere(1), // vertical stabilizer +// move([30,0,0])*scale([4,15,0.5]), mb_sphere(1), // horizontal stabilizer +// move([-15,0,0])*scale([6,45,0.5]), mb_sphere(1) // wing +// ]; +// voxel_size = 1; +// metaballs(spec, voxel_size, bounding_box); +// Example(3D): Demonstration of a custom metaball function, in this case a sphere with some random noise added to its value. The `dv` argument must be first; it is calculated internally as a distance vector from the metaball center to a probe point inside the bounding box, and you convert it to a scalar distance `dist` that is calculated inside your function (`dist` could be a more complicated expression, depending on the shape of the metaball). The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to include the `cutoff` and `influence` arguments in a custom function, but this example shows how. +// function noisy_sphere(dv, r, noise_level, cutoff=INF, influence=1) = +// let( +// noise = rands(0, noise_level, 1)[0], +// dist = norm(dv) + noise +// ) mb_cutoff(dist,cutoff) * (r/dist)^(1/influence); +// +// spec = [ +// left(9), mb_sphere(5), +// right(9), function (dv) noisy_sphere(dv, 5, 0.2), +// ]; +// voxelsize = 0.5; +// boundingbox = [[-16,-8,-8], [16,8,8]]; +// metaballs(spec, voxelsize, boundingbox); +// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A complex example using ellipsoids, a capsule, 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. +// include +// tetpts = zrot(15, p = 22 * regular_polyhedron_info("vertices", "tetrahedron")); +// tettransform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; +// +// spec = [ +// // vertical cylinder arm +// up(15), mb_capsule(17, 2, influence=0.8), +// // ellipsoid arms +// for(i=[0:2]) each [tettransform[i], mb_sphere(1, cutoff=30)], +// // ring on top +// up(35)*xrot(90), mb_torus(r_maj=8, r_min=2.5, cutoff=35), +// // feet +// for(i=[0:2]) each [move(2.2*tetpts[i]), mb_sphere(5, cutoff=30)], +// ]; +// voxelsize = 1; +// boundingbox = [[-22,-32,-13], [36,32,46]]; +// // useful to save as VNF for copies and manipulations +// vnf = metaballs(spec, voxelsize, boundingbox, isovalue=1); +// vnf_polyhedron(vnf); +// Example(3D,Med,NoAxes,VPR=[70,0,30],VPD=520,VPT=[0,0,80]): Example of grouping metaballs together and nesting them in lists of other metaballs, to make a crude model of a hand. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight ellipsoid dent subtracted to hollow out the palm, to make the hand. +// joints = [[0,0,1], [0,0,85], [0,-5,125], [0,-16,157], [0,-30,178]]; +// finger = [ +// for(i=[0:3]) each +// [IDENT, mb_connector(joints[i], joints[i+1], 9+i/5, influence=0.22)] +// ]; +// thumb = [ +// for(i=[0:2]) each [ +// scale([1,1,1.2]), +// mb_connector(joints[i], joints[i+1], 9+i/2, influence=.28) +// ] +// ]; +// allfingers = [ +// left(15)*zrot(5)*yrot(-50)*scale([1,1,0.6])*zrot(30), thumb, +// left(15)*yrot(-9)*scale([1,1,0.9]), finger, +// IDENT, finger, +// right(15)*yrot(8)*scale([1,1,0.92]), finger, +// right(30)*yrot(17)*scale([0.9,0.9,0.75]), finger +// ]; +// hand = [ +// IDENT, allfingers, +// move([-5,0,5])*scale([1,0.36,1.55]), mb_cuboid(90, squareness=0.3, cutoff=80), +// move([-10,-95,50])*yrot(10)*scale([2,2,0.95]), +// mb_sphere(r=15, cutoff=50, influence=1.5, negative=true) +// ]; +// voxsize=2.5; +// bbox = [[-104,-40,-10], [79,18,188]]; +// metaballs(hand, voxsize, bbox, isovalue=1); + +module metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false) { + vnf = metaballs(spec, voxel_size, bounding_box, isovalue, closed, show_stats); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); +} + +function metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, show_stats=false) = + assert(all_defined([spec, isovalue, bounding_box, voxel_size]), "\nThe parameters spec, isovalue, bounding_box, and voxel_size must all be defined.") + assert(len(spec)%2==0, "\nThe spec parameter must be an even-length list of alternating transforms and functions") + let( + funclist = _mb_unwind_list(spec), + nballs = len(funclist)/2, + dummycheck = [ + for(i=[0:len(spec)/2-1]) let(j=2*i) + assert(is_matrix(spec[j],4,4), str("\nspec entry at position ", j, " must be a 4×4 matrix.")) + assert(is_function(spec[j+1]) || is_list(spec[j+1]), str("\nspec entry at position ", j+1, " must be a function literal or a metaball list.")) 0 + ], + // set up transformation matrices in advance + transmatrix = [ + for(i=[0:nballs-1]) + let(j=2*i) + transpose(select(matrix_inverse(funclist[j]), 0,2)) + ], + + // new bounding box centered around original, forced to integer multiples of voxel size + halfvox = 0.5*voxel_size, + bbcenter = mean(bounding_box), + bbnums = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size), + newbbox = [bbcenter - halfvox*bbnums, bbcenter + halfvox*bbnums], + + // set up field array + bot = newbbox[0], + top = newbbox[1], + // accumulate metaball contributions using matrices rather than sums + xset = [bot.x:voxel_size:top.x+halfvox], + yset = list([bot.y:voxel_size:top.y+halfvox]), + zset = list([bot.z:voxel_size:top.z+halfvox]), + allpts = [for(x=xset, y=yset, z=zset) [x,y,z,1]], + trans_pts = [for(i=[0:nballs-1]) allpts*transmatrix[i]], + allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funclist[2*i+1](pt)]], + //total = _sum(allvals,allvals[0]*EPSILON), + total = _sum(slice(allvals,1,-1), allvals[0]), + fieldarray = list_to_matrix(list_to_matrix(total,len(zset)),len(yset)) + ) isosurface(fieldarray, isovalue, voxel_size, closed=closed, show_stats=show_stats, _mb_origin=newbbox[0]); + + +function _mb_unwind_list(list, parent_trans=[IDENT]) = + let( + dum1 = assert(is_list(list), "\nDid not find valid list of metaballs."), + n=len(list), + dum2 = assert(n%2==0, "\nList of metaballs must have an even number of elements with alternating transforms and functions/lists.") + ) [ + for(i=[0:2:n-1]) + let( + dum = assert(is_matrix(list[i],4,4), str("\nInvalid 4×4 transformation matrix found at position ",i,".")), + trans = parent_trans[0] * list[i], + j=i+1 + ) if(is_function(list[j])) + each [trans, list[j]] + else if (is_list(list[j])) + each _mb_unwind_list(list[j], [trans]) + else + assert(false, str("\nExpected function literal or list at position ",j,".")) + ]; + + + +/// ---------- isosurface stuff starts here ---------- + +// Function&Module: isosurface() +// Synopsis: Creates a 3D isosurface (a 3D contour) from a function or array of values. +// SynTags: Geom,VNF +// Topics: Isosurfaces, VNF Generators +// Usage: As a module +// isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [show_stats=]); +// Description: +// Computes a [VNF structure](vnf.scad) of a 3D isosurface within a bounded box at a single +// isovalue or range of isovalues. +// The isosurface of a function $f(x,y,z)$ is the set of points where $f(x,y,z)=c$ for some +// constant isovalue, $c$. +// To provide a function you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// taking three parameters as input to define the grid coordinate location (e.g. `x,y,z`) and +// returning a single numerical value. +// You can also define an isosurface using a 3D array of values instead of a function, in which +// case the isosurface is the set of points where the array is equal to the isovalue. The array +// indices are in the order `[x][y][z]`. +// . +// The VNF that is computed has the isosurface as its bounding surface, with all the points where +// $f(x,y,z)>c$ on the interior side of the surface. +// When the isovalue is a range, `[c1, c2]`, then the resulting VNF has two bounding surfaces +// corresponding to `c1` and `c2`, and the interior of the object are the points with intermediate +// isovalues; this generally produces a shell object that has an inside and outside surface. The +// range can start at `-INF` or end at `INF`. A single isovalue `c` is equivalent to `[c,INF]`. +// . +// The isosurface is evaluated over a bounding box defined by its minimum and maximum corners, +// `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. This bounding box is divided into voxels of the +// specified `voxel_size`. Smaller voxels produce a finer, smoother result at the expense of +// execution time. If the voxel size doesn't exactly divide your specified bounding box, then +// the bounding box is enlarged to contain whole voxels, and centered on your requested box. If +// the bounding box clips the isosurface and `closed=true` (the default) a surface is added to create +// a closed manifold object. Setting `closed=false` causes the VNF to end at the bounding box, +// resulting in a non-manifold shape with holes, exposing the inside of the object. +// . +// The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. +// A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the +// calculation and storage of 8,000,000 function values, and more processing and memory to generate +// the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box +// requires only 8,000 function values and a modest computation time. A good rule is to keep the +// number of voxels below 10,000 for preview, and adjust the voxel size smaller for final +// rendering. A bounding box that is larger than your isosurface wastes time computing function +// values that are not needed. If the isosurface fits completely within the bounding box, you can +// call {{pointlist_bounds()}} on `vnf[0]` returned from the `isosurface()` function to get an +// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the +// same run time but increase the resolution. You can also set the parameter `show_stats=true` to +// get the bounds of the voxels containing the surface. +// . +// 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 (often +// resulting from clipping by the bounding box) 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 computationall expensive +// and are not normally necessary. +// Arguments: +// f = The isosurface function or array. +// isovalue = a scalar giving the isovalue parameter or a 2-vector giving an isovalue range. +// voxel_size = scalar size of the voxel cube that is used to sample the surface. +// bounding_box = When `f` is a function, a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. +// --- +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true +// reverse = When true, reverses the orientation of the VNF faces. Default: false +// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that contain the surface, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false +// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// 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 parameter 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = 0; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox, +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = 0; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox); +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox, +// 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*x, py = p*y, pz = p*z +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, voxel_size=5, bounding_box=bbox); +// Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). +// function schwartz_p(x,y,z, wavelength) = let( +// p = 360/wavelength, +// px = p*x, py = p*y, pz = p*z +// ) cos(px) + cos(py) + cos(pz); +// isovalue = [-0.2, 0.2]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) schwartz_p(x,y,z, 100), +// isovalue, voxel_size=4, bounding_box=bbox); +// Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). +// function neovius(x,y,z, wavelength) = let( +// p = 360/wavelength, +// px = p*x, py = p*y, pz = p*z +// ) 3*(cos(px) + cos(py) + cos(pz)) + 4*cos(px)*cos(py)*cos(pz); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) neovius(x,y,z,200), +// isovalue, voxel_size=4, bounding_box=bbox); +// Example(3D): Using an array for the `f` argument instead of a function literal. {{metaballs()}} also makes use of this feature, calculating the the 3D grid first. +// field = [ +// repeat(0,[6,6]), +// [ [0,1,2,2,1,0], +// [1,2,3,3,2,1], +// [2,3,4,4,3,2], +// [2,3,4,4,3,2], +// [1,2,3,3,2,1], +// [0,1,2,2,1,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,1,1,0,0], +// [0,2,3,3,2,0], +// [0,2,3,3,2,0], +// [0,0,1,1,0,0], +// [0,0,0,0,0,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,0,0,0,0], +// [0,1,2,2,1,0], +// [0,1,2,2,1,0], +// [0,0,0,0,0,0], +// [0,0,0,0,0,0] +// ], +// repeat(0,[6,6]) +// ]; +// rotate([0,-90,180]) +// isosurface(field, isovalue=0.5, +// voxel_size=10); + +module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _mb_origin=undef) { + vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, show_stats, _mb_origin); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); +} + +function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, show_stats=false, _mb_origin=undef) = + assert(all_defined([f, isovalue, voxel_size]), "\nThe parameters f, isovalue, and bounding_box must all be defined.") + assert((is_function(f) && is_def(bounding_box)) || (is_list(f) && is_undef(bounding_box)), + "\nbounding_box must be passed if f is a function, and cannot be passed if f is an array.") + let( + isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, + isovalmax = is_list(isovalue) ? isovalue[1] : INF, + dum1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")), + hv = 0.5*voxel_size, + bbox = is_function(f) + ? let( // new bounding box quantized for voxel_size, centered around original box + bbcenter = mean(bounding_box), + bbn = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size) + ) [bbcenter - hv*bbn, bbcenter + hv*bbn] + : let( // new bounding box, either centered on origin or using metaball origin + dims = list_shape(f) - [1,1,1] + ) is_def(_mb_origin) + ? [_mb_origin, _mb_origin+voxel_size*dims] // metaball bounding box + : [-hv*dims, hv*dims], // centered bounding box + cubes = _isosurface_cubes(voxel_size, bbox, + fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, + 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), + faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], + dum2 = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 +) [trianglepoints, faces]; From daa68397a44fe20981749e10cba0d84de0091adf Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Fri, 21 Feb 2025 17:28:27 -0800 Subject: [PATCH 09/11] Update isosurface.scad --- isosurface.scad | 989 +++++++++++++++++++++++++++++++----------------- 1 file changed, 632 insertions(+), 357 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index dde97766..d1a0429e 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -77,50 +77,8 @@ Vertex and edge layout (heavier = and # indicate closer to viewer): 0 +==========+ 4 +=====8=====+ z changes fastest, then y, then x. - ------------------------------------------------------------ -Addition by Alex: -Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. - - 4(back) - 3 +----------+ 7 - /: 5(top) /| - / : / | - 1 +==========+5 | <-- 3(side) -0(side) --> # 2+ - - - # -+ 6 - # / # / - #/ 2(bot) #/ - 0 +----------+ 4 - 1(front) */ -/// four indices for each face of the cube, counterclockwise looking from inside out -_MCFaceVertexIndices = [ - [], - [0,2,3,1], // left, x=0 plane - [0,1,5,4], // front, y=0 plane - [0,4,6,2], // bottom, z=0 plane - [4,5,7,6], // right, x=voxsize plane - [2,6,7,3], // back, y=voxsize plane - [1,3,7,5], // top, z=voxsize plane -]; - -/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. -function _bbox_faces(v0, voxsize, bbox) = let( - a = v0-bbox[0], - bb1 = bbox[1] - [voxsize,voxsize,voxsize], - b = v0-bb1 -) [ - if(a[0]==0) 1, - if(a[1]==0) 2, - if(a[2]==0) 3, - if(b[0]>=0) 4, - if(b[1]>=0) 5, - if(b[2]>=0) 6 -]; -/// End of bounding-box face-clipping stuff. Back to the marching cubes triangulation.... - - /// Pair of vertex indices for each edge on the voxel _MCEdgeVertexIndices = [ [0, 1], @@ -662,14 +620,105 @@ _MCTriangleTable_reverse = [ /// _cubindex() - private function, called by _isosurface_cubes() /// Return the index ID of a voxel depending on the field strength at each corner exceeding isoval. function _cubeindex(f, isoval) = + (f[0] >= isoval ? 1 : 0) + + (f[1] >= isoval ? 2 : 0) + + (f[2] >= isoval ? 4 : 0) + + (f[3] >= isoval ? 8 : 0) + + (f[4] >= isoval ? 16 : 0) + + (f[5] >= isoval ? 32 : 0) + + (f[6] >= isoval ? 64 : 0) + + (f[7] >= isoval ? 128 : 0); + +/* +----------------------------------------------------------- +Bounding box clipping support: + +Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. + + 4(back) + 3 +----------+ 7 + /: 5(top) /| + / : / | + 1 +==========+5 | <-- 3(side) +0(side) --> # 2+ - - - # -+ 6 + # / # / + #/ 2(bot) #/ + 0 +----------+ 4 + 1(front) + +The clip face uses different indexing. After vertex coordinates and function values are assigned to each corner from the original voxel based on _MCFaceVertexIndices below, this is the clip face diagram: + +(1) (2) + +----1----+ + | | + 0 2 + | | + +----3----+ +(0) (3) +*/ + +/// four indices for each face of the cube, counterclockwise looking from inside out +_MCFaceVertexIndices = [ + [], + [0,2,3,1], // left, x=0 plane + [0,1,5,4], // front, y=0 plane + [0,4,6,2], // bottom, z=0 plane + [4,5,7,6], // right, x=voxsize plane + [2,6,7,3], // back, y=voxsize plane + [1,3,7,5], // top, z=voxsize plane +]; + +/* +/// Pair of vertex indices for each edge on the clip face (using clip face indexing) +_MCClipEdgeVertexIndices = [ + [0,1], [1,2], [2,3], [3,0] +]; + +/// For each of the 16 configurations of a clip face, define a list of triangles, specified as pairs of corner ID and edge ID arrays, with a total of 3 points in each pair. Each pair has the form [corner],[edge1,edge2] or [corner1,corner2],[edge]. +/// In keeping with the convention for triangulating an isosurface through a voxel, analogous to the case in which two surfaces separate two diagonally opposite greater-than-isovalue corners of one face, in 2D contour terms it is assumed there is a valley separating two diagonally-opposite high corners, not a ridge connecting them. The two triangulation cases for opposing corners are set up accordingly. +_MCClipTriangleTable = [ + [], // 0 - 0000 - ignored + [[0],[0,3]], // 1 - 0001 + [[1],[1,0]], // 2 - 0010 + [[0,1],[1], [0],[1,3]], // 3 - 0011 + [[2],[2,1]], // 4 - 0100 + [[0],[0,3], [2],[2,1]], // 5 - 0101 - opposing corners + [[1,2],[0], [2],[2,0]], // 6 - 0110 + [[0,1],[3], [1],[2,3], [1,2],[2]], // 7 - 0111 + [[3],[3,2]], // 8 - 1000 + [[3,0],[0], [3],[0,2]], // 9 - 1001 + [[1],[1,0], [3],[3,2]], //10 - 1010 - opposing corners + [[0,1],[1], [0],[1,2], [3,0],[2]], //11 - 1011 + [[2,3],[3], [2],[3,1]], //12 - 1100 + [[3,0],[0], [3],[0,1], [2,3],[1]], //13 - 1101 + [[2,3],[3], [2],[3,0], [1,2],[0]], //14 - 1110 + [[0,1,2],[], [0,2,3],[]], //15 - 1111 +]; + +/// _clipfacindex() - private function, called by _clipfacevertices() +/// Return the index ID of a voxel face depending on the field strength at each corner exceeding isoval. +function _clipfacindex(f, isoval) = (f[0] > isoval ? 1 : 0) + (f[1] > isoval ? 2 : 0) + (f[2] > isoval ? 4 : 0) + - (f[3] > isoval ? 8 : 0) + - (f[4] > isoval ? 16 : 0) + - (f[5] > isoval ? 32 : 0) + - (f[6] > isoval ? 64 : 0) + - (f[7] > isoval ? 128 : 0); + (f[3] > isoval ? 8 : 0); +*/ + +/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. voxsize is a 3-vector. +function _bbox_faces(v0, voxsize, bbox) = let( + a = v_abs(v0-bbox[0]), + bb1 = bbox[1] - voxsize, + b = v0-bb1 +) [ + if(a[0]=-EPSILON) 4, + if(b[1]>=-EPSILON) 5, + if(b[2]>=-EPSILON) 6 +]; +/// End of bounding-box face-clipping stuff +/// ----------------------------------------------------------- /// isosurface_cubes() - private function, called by isosurface() @@ -682,17 +731,17 @@ function _cubeindex(f, isoval) = /// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections. /// cf (corner function) is vector containing the 8 field strength values at each corner of the voxel cube. /// 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. +/// The bounding box 'bbox' is expected to be quantized for the voxel size already, and `voxsize` is a 3-vector. function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isovalmax, closed=true) = let( // get field intensities field = is_def(fieldarray) ? fieldarray - : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+[hv,hv,hv]) [ - for(x=[v.x:voxsize:b1.x]) [ - for(y=[v.y:voxsize:b1.y]) [ - for(z=[v.z:voxsize:b1.z]) - fieldfunc(x,y,z) + : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+hv) [ + for(x=[v.x:voxsize.x:b1.x]) [ + for(y=[v.y:voxsize.y:b1.y]) [ + for(z=[v.z:voxsize.z:b1.z]) + fieldfunc([x,y,z]) ] ] ], @@ -701,9 +750,9 @@ function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isov nz = len(field[0][0])-2, v0 = bbox[0] ) [ - for(i=[0:nx]) let(x=v0[0]+voxsize*i) - for(j=[0:ny]) let(y=v0[1]+voxsize*j) - for(k=[0:nz]) let(z=v0[2]+voxsize*k) + for(i=[0:nx]) let(x=v0[0]+i*voxsize.x) + for(j=[0:ny]) let(y=v0[1]+j*voxsize.y) + for(k=[0:nz]) let(z=v0[2]+k*voxsize.z) let(i1=i+1, j1=j+1, k1=k+1, cf = [ // cube corner field values clamped to ±1e9 min(1e9,max(-1e9,field[i][j][k])), @@ -719,12 +768,12 @@ function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isov maxcf = max(cf), cubecoord = [x,y,z], bfaces = closed ? _bbox_faces(cubecoord, voxsize, bbox) : [], - cubefound_isomin = (mincf<=isovalmin && isovalmin0) for(ei=tritablemin[cbidxmin]) // min surface @@ -778,13 +823,52 @@ function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritabl u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), - if(len(outfacevertices)>0) for(bf = bbfaces) + if(len(bbfaces)>0) for(bf = bbfaces) each _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) ] ]; +/* /// Generate triangles for the special case of voxel faces clipped by the bounding box +// (more efficient than _bbfacevertices below but doesn't work with isovalue ranges) +function _clipfacevertices(vcube, f, bbface, isovalmax, isovalmin) = + let( + vi = _MCFaceVertexIndices[bbface], // four voxel face vertex indices + vfc = [ for(i=vi) vcube[i] ], // four voxel face vertex coordinates + fld = [ for(i=vi) f[i] ], // four corner field values + minidx = _clipfacindex(fld, isovalmin), + maxidx = _clipfacindex(fld, isovalmax) + ) [ + if(minidx>0) + let(tabl = _MCClipTriangleTable[minidx]) + for(i=[0:2:len(tabl)-1]) each [ + for(c=tabl[i]) vfc[c], + for(ei=tabl[i+1]) let( + edge = _MCClipEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = fld[vi1] - fld[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-fld[vi0]) / denom + ) vfc[vi0] + u*(vfc[vi1]-vfc[vi0]) + ], + if(false && maxidx>0) + let(tabl = _MCClipTriangleTable[maxidx]) + for(i=[0:2:len(tabl)-1]) each [ + for(c=tabl[i]) vfc[c], + for(ei=tabl[i+1]) let( + edge = _MCClipEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = fld[vi1] - fld[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-fld[vi0]) / denom + ) vfc[vi0] + u*(vfc[vi1]-vfc[vi0]) + ] + ]; +*/ + +/// Generate triangles for the special case of voxel faces clipped by the bounding box +/// TODO: Address isolated manifold error in edge case where two different isosurfaces intersect the same voxel AND that voxel is on a box boundary. This can be contrived but hasn't yet come up in actual testing. function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( vi = _MCFaceVertexIndices[bbface], // four voxel face vertex indices //vfc = [ for(i=vi) vcube[i] ], // four voxel face vertex coordinates @@ -827,24 +911,26 @@ function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( ]) flatten(triangles); -/// _showstats() (Private function) - called by isosurface() and metaballs() +/// _showstats_isosurface() (Private function) - called by isosurface() /// Display statistics about isosurface -function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( +function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) = let( v = column(cubes, 0), // extract cube vertices x = column(v,0), // extract x values y = column(v,1), // extract y values z = column(v,2), // extract z values xmin = min(x), - xmax = max(x)+voxelsize, + xmax = max(x)+voxsize.x, ymin = min(y), - ymax = max(y)+voxelsize, + ymax = max(y)+voxsize.y, zmin = min(z), - zmax = max(z)+voxelsize, + zmax = max(z)+voxsize.z, ntri = len(faces), nvox = len(cubes) -) echo(str("\nIsosurface statistics:\n Outer isovalue = ", isoval, "\n Voxel size = ", voxelsize, - "\n Voxels found containing surface = ", nvox, "\n Triangles = ", ntri, - "\n Voxel bounding box for all data = ", bbox, +) echo(str("\nIsosurface statistics:\n Isovalue = ", isoval, "\n Voxel size = ", voxsize, + "\n Voxels intersected by the surface = ", nvox, + "\n Triangles = ", ntri, + "\n VNF bounds = ", pointlist_bounds(triangles), + "\n Bounds for all data = ", bbox, "\n Voxel bounding box for isosurface = ", [[xmin,ymin,zmin], [xmax,ymax,zmax]], "\n")) 0; @@ -864,11 +950,11 @@ function mb_cutoff(dist, cutoff) = dist>=cutoff ? 0 : 0.5*(cos(180*(dist/cutoff) /// metaball sphere -function _mb_sphere_basic(dv, r, neg) = neg*r/norm(dv); -function _mb_sphere_influence(dv, r, ex, neg) = neg * (r/norm(dv))^ex; -function _mb_sphere_cutoff(dv, r, cutoff, neg) = let(dist=norm(dv)) +function _mb_sphere_basic(point, r, neg) = neg*r/norm(point); +function _mb_sphere_influence(point, r, ex, neg) = neg * (r/norm(point))^ex; +function _mb_sphere_cutoff(point, r, cutoff, neg) = let(dist=norm(point)) neg * mb_cutoff(dist, cutoff) * r/dist; -function _mb_sphere_full(dv, r, cutoff, ex, neg) = let(dist=norm(dv)) +function _mb_sphere_full(point, r, cutoff, ex, neg) = let(dist=norm(point)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = @@ -879,34 +965,34 @@ function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_sphere_basic(dv,r,neg) - : !is_finite(cutoff) ? function(dv) _mb_sphere_influence(dv,r,1/influence, neg) - : influence==1 ? function(dv) _mb_sphere_cutoff(dv,r,cutoff,neg) - : function(dv) _mb_sphere_full(dv,r,cutoff,1/influence,neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_sphere_basic(point,r,neg) + : !is_finite(cutoff) ? function (point) _mb_sphere_influence(point,r,1/influence, neg) + : influence==1 ? function (point) _mb_sphere_cutoff(point,r,cutoff,neg) + : function (point) _mb_sphere_full(point,r,cutoff,1/influence,neg); /// metaball rounded cube -function _mb_cuboid_basic(dv, inv_size, xp, neg) = +function _mb_cuboid_basic(point, inv_size, xp, neg) = let( - dv=inv_size * dv, - dist = xp >= 1100 ? max(v_abs(dv)) - : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) + point=inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + : (abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) ) neg/dist; -function _mb_cuboid_influence(dv, inv_size, xp, ex, neg) = let( - dv=inv_size * dv, - dist = xp >= 1100 ? max(v_abs(dv)) - :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +function _mb_cuboid_influence(point, inv_size, xp, ex, neg) = let( + point=inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + :(abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) ) neg / dist^ex; -function _mb_cuboid_cutoff(dv, inv_size, xp, cutoff, neg) = let( - dv = inv_size * dv, - dist = xp >= 1100 ? max(v_abs(dv)) - : (abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +function _mb_cuboid_cutoff(point, inv_size, xp, cutoff, neg) = let( + point = inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + : (abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) ) neg * mb_cutoff(dist, cutoff) / dist; -function _mb_cuboid_full(dv, inv_size, xp, ex, cutoff, neg) = let( - dv = inv_size * dv, - dist = xp >= 1100 ? max(v_abs(dv)) - :(abs(dv.x)^xp + abs(dv.y)^xp + abs(dv.z)^xp) ^ (1/xp) +function _mb_cuboid_full(point, inv_size, xp, ex, cutoff, neg) = let( + point = inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + :(abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) ) neg * mb_cutoff(dist, cutoff) / dist^ex; function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = @@ -919,17 +1005,17 @@ function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false inv_size = is_num(size) ? 2/size : [[2/size.x,0,0],[0,2/size.y,0],[0,0,2/size.z]] ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_cuboid_basic(dv, inv_size, xp, neg) - : !is_finite(cutoff) ? function(dv) _mb_cuboid_influence(dv, inv_size, xp, 1/influence, neg) - : influence==1 ? function(dv) _mb_cuboid_cutoff(dv, inv_size, xp, cutoff, neg) - : function (dv) _mb_cuboid_full(dv, inv_size, xp, 1/influence, cutoff, neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_cuboid_basic(point, inv_size, xp, neg) + : !is_finite(cutoff) ? function(point) _mb_cuboid_influence(point, inv_size, xp, 1/influence, neg) + : influence==1 ? function(point) _mb_cuboid_cutoff(point, inv_size, xp, cutoff, neg) + : function (point) _mb_cuboid_full(point, inv_size, xp, 1/influence, cutoff, neg); /// metaball rounded cylinder / cone -function _revsurf_basic(dv, path, coef, neg) = +function _revsurf_basic(point, path, coef, neg) = let( - pt = [norm([dv.x,dv.y]), dv.z], + pt = [norm([point.x,point.y]), point.z], segs = pair(path), dist = min([for(seg=segs) let( @@ -945,9 +1031,9 @@ function _revsurf_basic(dv, path, coef, neg) = ) neg * (inside_check==[] ? coef*(1+dist) : coef/(1+dist)); -function _revsurf_influence(dv, path, coef, exp, neg) = +function _revsurf_influence(point, path, coef, exp, neg) = let( - pt = [norm([dv.x,dv.y]), dv.z], + pt = [norm([point.x,point.y]), point.z], segs = pair(path), dist = min([for(seg=segs) let( @@ -963,9 +1049,9 @@ function _revsurf_influence(dv, path, coef, exp, neg) = ) neg * (inside_check==[] ? (coef*(1+dist))^exp : (coef/(1+dist))^exp); -function _revsurf_cutoff(dv, path, coef, cutoff, neg) = +function _revsurf_cutoff(point, path, coef, cutoff, neg) = let( - pt = [norm([dv.x,dv.y]), dv.z], + pt = [norm([point.x,point.y]), point.z], segs = pair(path), dist = min([for(seg=segs) let( @@ -982,9 +1068,9 @@ function _revsurf_cutoff(dv, path, coef, cutoff, neg) = neg * (inside_check==[] ? (coef*(1+dist)) : mb_cutoff(dist-coef, cutoff) * (coef/(1+dist)) ); -function _revsurf_full(dv, path, coef, cutoff, exp, neg) = +function _revsurf_full(point, path, coef, cutoff, exp, neg) = let( - pt = [norm([dv.x,dv.y]), dv.z], + pt = [norm([point.x,point.y]), point.z], segs = pair(path), dist = min([for(seg=segs) let( @@ -1024,29 +1110,29 @@ function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influe assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") assert(roundlen1+roundlen2 < facelen, "Roundings don't fit on the edge length of the cylinder/cone") let(shifted = offset(sides, delta=-rounding, closed=false)) - !is_finite(cutoff) && influence==1 ? function(dv) _revsurf_basic(dv, shifted, 1+rounding, neg) - : !is_finite(cutoff) ? function(dv) _revsurf_influence(dv, shifted, 1+rounding, 1/influence, neg) - : influence==1 ? function(dv) _revsurf_cutoff(dv, shifted, 1+rounding, cutoff, neg) - : function (dv) _revsurf_full(dv, shifted, 1+rounding, cutoff, 1/influence, neg); + !is_finite(cutoff) && influence==1 ? function(point) _revsurf_basic(point, shifted, 1+rounding, neg) + : !is_finite(cutoff) ? function(point) _revsurf_influence(point, shifted, 1+rounding, 1/influence, neg) + : influence==1 ? function(point) _revsurf_cutoff(point, shifted, 1+rounding, cutoff, neg) + : function (point) _revsurf_full(point, shifted, 1+rounding, cutoff, 1/influence, neg); /// metaball capsule (round-ended cylinder) -function _mb_capsule_basic(dv, hl, r, neg) = let( - dist = dv.z<-hl ? norm(dv-[0,0,-hl]) - : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +function _mb_capsule_basic(point, hl, r, neg) = let( + dist = point.z<-hl ? norm(point-[0,0,-hl]) + : point.z<=hl ? norm([point.x,point.y]) : norm(point-[0,0,hl]) ) neg*r/dist; -function _mb_capsule_influence(dv, hl, r, ex, neg) = let( - dist = dv.z<-hl ? norm(dv-[0,0,-hl]) - : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +function _mb_capsule_influence(point, hl, r, ex, neg) = let( + dist = point.z<-hl ? norm(point-[0,0,-hl]) + : point.z<=hl ? norm([point.x,point.y]) : norm(point-[0,0,hl]) ) neg * (r/dist)^ex; -function _mb_capsule_cutoff(dv, hl, r, cutoff, neg) = let( - dist = dv.z<-hl ? norm(dv-[0,0,-hl]) - : dv.z0, "\nTotal length must accommodate rounded ends of cylinder."), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_capsule_basic(dv,sh/2,r,neg) - : !is_finite(cutoff) ? function(dv) _mb_capsule_influence(dv,sh/2,r,1/influence, neg) - : influence==1 ? function(dv) _mb_capsule_cutoff(dv,sh/2,r,cutoff,neg) - : function (dv) _mb_capsule_full(dv, sh/2, r, cutoff, 1/influence, neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_capsule_basic(point,sh/2,r,neg) + : !is_finite(cutoff) ? function(point) _mb_capsule_influence(point,sh/2,r,1/influence, neg) + : influence==1 ? function(point) _mb_capsule_cutoff(point,sh/2,r,cutoff,neg) + : function (point) _mb_capsule_full(point, sh/2, r, cutoff, 1/influence, neg); /// metaball disk with rounded edge -function _mb_disk_basic(dv, hl, r, neg) = +function _mb_disk_basic(point, hl, r, neg) = let( - rdist=norm([dv.x,dv.y]), - dist = rdist0, "\nDiameter must be greater than height."), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_disk_basic(dv,h2,r,neg) - : !is_finite(cutoff) ? function(dv) _mb_disk_influence(dv,h2,r,1/influence, neg) - : influence==1 ? function(dv) _mb_disk_cutoff(dv,h2,r,cutoff,neg) - : function (dv) _mb_disk_full(dv, h2, r, cutoff, 1/influence, neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_disk_basic(point,h2,r,neg) + : !is_finite(cutoff) ? function(point) _mb_disk_influence(point,h2,r,1/influence, neg) + : influence==1 ? function(point) _mb_disk_cutoff(point,h2,r,cutoff,neg) + : function (point) _mb_disk_full(point, h2, r, cutoff, 1/influence, neg); /// metaball connector cylinder - calls mb_capsule* functions after transform @@ -1126,31 +1212,31 @@ function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = h = norm(dc)/2, // center-to-center length (cylinder height) transform = submatrix(down(h)*rot(from=dc,to=UP)*move(-p1) ,[0:2], [0:3]) ) - !is_finite(cutoff) && influence==1 ? function(dv) - let(newdv = transform * [each dv,1]) - _mb_capsule_basic(newdv,h,r,neg) - : !is_finite(cutoff) ? function(dv) - let(newdv = transform * [each dv,1]) - _mb_capsule_influence(newdv,h,r,1/influence, neg) - : influence==1 ? function(dv) - let(newdv = transform * [each dv,1]) - _mb_capsule_cutoff(newdv,h,r,cutoff,neg) - : function (dv) - let(newdv = transform * [each dv,1]) - _mb_capsule_full(newdv, h, r, cutoff, 1/influence, neg); + !is_finite(cutoff) && influence==1 ? function(point) + let(newpoint = transform * [each point,1]) + _mb_capsule_basic(newpoint,h,r,neg) + : !is_finite(cutoff) ? function(point) + let(newpoint = transform * [each point,1]) + _mb_capsule_influence(newpoint,h,r,1/influence, neg) + : influence==1 ? function(point) + let(newpoint = transform * [each point,1]) + _mb_capsule_cutoff(newpoint,h,r,cutoff,neg) + : function (point) + let(newpoint = transform * [each point,1]) + _mb_capsule_full(newpoint, h, r, cutoff, 1/influence, neg); /// metaball torus -function _mb_torus_basic(dv, rmaj, rmin, neg) = - let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg*rmin/dist; -function _mb_torus_influence(dv, rmaj, rmin, ex, neg) = - let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) neg * (rmin/dist)^ex; -function _mb_torus_cutoff(dv, rmaj, rmin, cutoff, neg) = - let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) +function _mb_torus_basic(point, rmaj, rmin, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg*rmin/dist; +function _mb_torus_influence(point, rmaj, rmin, ex, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg * (rmin/dist)^ex; +function _mb_torus_cutoff(point, rmaj, rmin, cutoff, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg * mb_cutoff(dist, cutoff) * rmin/dist; -function _mb_torus_full(dv, rmaj, rmin, cutoff, ex, neg) = - let(dist = norm([norm([dv.x,dv.y])-rmaj, dv.z])) +function _mb_torus_full(point, rmaj, rmin, cutoff, ex, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min, or,od,ir,id) = @@ -1172,22 +1258,22 @@ function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, assert(false, "\nBad minor size parameter."), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_torus_basic(dv, r_maj, r_min, neg) - : !is_finite(cutoff) ? function(dv) _mb_torus_influence(dv, r_maj, r_min, 1/influence, neg) - : influence==1 ? function(dv) _mb_torus_cutoff(dv, r_maj, r_min, cutoff, neg) - : function(dv) _mb_torus_full(dv, r_maj, r_min, cutoff, 1/influence, neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_torus_basic(point, r_maj, r_min, neg) + : !is_finite(cutoff) ? function(point) _mb_torus_influence(point, r_maj, r_min, 1/influence, neg) + : influence==1 ? function(point) _mb_torus_cutoff(point, r_maj, r_min, cutoff, neg) + : function(point) _mb_torus_full(point, r_maj, r_min, cutoff, 1/influence, neg); /// metaball octahedron -function _mb_octahedron_basic(dv, r, neg) = - let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg*r/dist; -function _mb_octahedron_influence(dv, r, ex, neg) = - let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * (r/dist)^ex; -function _mb_octahedron_cutoff(dv, r, cutoff, neg) = - let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * r/dist; -function _mb_octahedron_full(dv, r, cutoff, ex, neg) = - let(dist = abs(dv.x) + abs(dv.y) + abs(dv.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; +function _mb_octahedron_basic(point, r, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg*r/dist; +function _mb_octahedron_influence(point, r, ex, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * (r/dist)^ex; +function _mb_octahedron_cutoff(point, r, cutoff, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_octahedron_full(point, r, cutoff, ex, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") @@ -1197,10 +1283,10 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), neg = negative ? -1 : 1 ) - !is_finite(cutoff) && influence==1 ? function(dv) _mb_octahedron_basic(dv,r,neg) - : !is_finite(cutoff) ? function(dv) _mb_octahedron_influence(dv,r,1/influence, neg) - : influence==1 ? function(dv) _mb_octahedron_cutoff(dv,r,cutoff,neg) - : function(dv) _mb_octahedron_full(dv,r,cutoff,1/influence,neg); + !is_finite(cutoff) && influence==1 ? function(point) _mb_octahedron_basic(point,r,neg) + : !is_finite(cutoff) ? function(point) _mb_octahedron_influence(point,r,1/influence, neg) + : influence==1 ? function(point) _mb_octahedron_cutoff(point,r,cutoff,neg) + : function(point) _mb_octahedron_full(point,r,cutoff,1/influence,neg); // Function&Module: metaballs() @@ -1209,9 +1295,9 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // Topics: Metaballs, Isosurfaces, VNF Generators // See Also: isosurface() // Usage: As a module -// metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [convexity=], [show_stats=], ...) [ATTACHMENTS]; +// metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [auto_voxels=], [convexity=], [show_stats=], ...) [ATTACHMENTS]; // Usage: As a function -// vnf = metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [convexity=], [show_stats=]); +// vnf = metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [auto_voxels=], [convexity=], [show_stats=]); // Description: // ![Metaball animation](https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/images/metaball_demo.gif) // . @@ -1232,6 +1318,8 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // scaling to produce an ellipsoid from a sphere, and you can even use {{skew()}} if desired. // When no transformation is needed, give `IDENT` as the transformation. // . +// When multiple metaballs are in a model, their functions are summed and compared to the isovalue to +// determine the final shape of the metaball object. // The metaballs are evaluated over a bounding box defined by its minimum and maximum corners, // `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. The contributions from **all** metaballs, even those outside // the bounds, are evaluated over the bounding box. This bounding box is divided into voxels of the @@ -1248,7 +1336,7 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // . // You can create metaballs in a variety of standard shapes using the predefined functions // listed below. If you wish, you can also create custom metaball shapes using your own functions -// (see Example 19). For all of the built-in metaballs, three parameters are availableto control the +// (see Examples 20 and 21). For all of the built-in metaballs, three parameters are availableto control the // interaction of the metaballs with each other: `cutoff`, `influence`, and `negative`. // . // The `cutoff` parameter specifies the distance beyond which the metaball has no interaction @@ -1257,7 +1345,7 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // zero at the cutoff. Note that the smooth decrease may cause the interaction to become negligible // closer than the actual cutoff distance, depending on the voxel size and `influence` of the // ball. Also, depending on the value of `influence`, a cutoff that ends in the middle of -// another ball can result in strange shapes, as shown in Example 16, with the metaball +// another ball can result in strange shapes, as shown in Example 17, with the metaball // interacting on one side of the boundary and not interacting on the other side. If you scale // a ball, the cutoff value is also scaled. The exact way that cutoff is defined // geometrically varies for different ball types; see below for details. @@ -1272,14 +1360,9 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // distances, and you may want to set the `cutoff` argument to limit the range influence. // . // The `negative` parameter, if set to `true`, creates a negative metaball, which can result in -// hollows or dents in other metaballs, or swallow other metaballs almost entirely. -// Negative metaballs are always below the isovalue, so they are never directly visible; -// only their effects are visible. See Examples 15 and 16. -// . -// The `isovalue` parameter in `metaballs()` defaults to 1. If you increase it, then all the objects -// in your model shrink, causing some melded objects to separate. If you decrease it, each metaball -// grows and melds more with others. Be aware that changing the isovalue affects **all** the metaballs -// and changes the entire model, possibly dramatically. +// hollows or dents in other metaballs, or swallow other metaballs almost entirely. +// Negative metaballs are never directly visible; only their effects are visible. The `influence` +// argument may also behave in ways you don't expect with a negative metaball. See Examples 16 and 17. // . // For complicated metaball assemblies you may wish to repeat a structure in different locations or // otherwise transformed. Nested metaball specifications are supported: @@ -1288,7 +1371,12 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // `hand=[u0,finger,u1,finger,...]` and then invoke `metaballs()` with `[s0, hand]`. // In effect, any metaball specification array can be treated as a single metaball in another specification array. // This is a powerful technique that lets you make groups of metaballs that you can use as individual -// metaballs in other groups, and can make your code compact and simpler to understand. See Example 21. +// metaballs in other groups, and can make your code compact and simpler to understand. See Example 23. +// . +// Be aware that the `isovalue` parameter in `metaballs()` applies globally to **all** metaballs and defaults +// to 1. Changing the isovalue changes the entire model, possibly dramatically. If you increase the +// isovalue, then all the objects in your model shrink, causing some melded objects to separate. If you +// decrease it, each metaball grows and melds more with others. // . // ***Built-in metaball functions*** // . @@ -1302,11 +1390,6 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // specified sizes, even in isolation, if `isovalue < 1` and smaller than their specified sizes if // `isovalue > 1`. // . -// All of the built-in functions accept these named arguments, which are not repeated in the list below: -// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center unless otherwise noted below. Default: INF -// * `influence` — a positive number specifying the strength of interaction this ball has with other balls. Default: 1 -// * `negative` — when true, creates a negative metaball. Default: false -// . // The built-in metaball functions are listed below. As usual, arguments without a trailing `=` can be used positionally; arguments with a trailing `=` must be used as named arguments. // The examples below illustrates each type of metaball interacting with another of the same type. // . @@ -1317,16 +1400,27 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // * `mb_capsule(h|l|height|length, r|d=)` — cylinder of radius `r` or diameter `d` with hemispherical caps. The height or length specifies the total height including the rounded ends. // * `mb_connector(p1, p2, r|d=)` — a connecting rod of radius `r` or diameter `d` with hemispherical caps (like `mb_capsule()`), but specified to connect point `p1` to point `p2` (where `p1` and `p2` must be different 3D coordinates). The specified points are at the centers of the two capping hemispheres. You may want to set `influence` quite low; the connectors themselves are still influenced by other metaballs, but it may be undesirable to have them influence others, or each other. If two connectors are connected, the joint may appear swollen unless `influence` is reduced. // * `mb_torus([r_maj|d_maj=], [r_min|d_min=], [or=|od=], [ir=|id=])` — torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. Both major and minor radius/diameter must be specified regardless of how they are named. -// *`mb_octahedron(r|d=])` — octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip, while `d=` is the distance between two opposite tips. +// * `mb_octahedron(r|d=])` — octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip, while `d=` is the distance between two opposite tips. +// . +// In addition to the dimensional arguments described above, all of the built-in functions accept the +// following named arguments: +// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center unless otherwise noted below. Default: INF +// * `influence` — a positive number specifying the strength of interaction this ball has with other balls. Default: 1 +// * `negative` — when true, creates a negative metaball. Default: false // . // ***Metaball functions and user defined functions*** // . +// You can construct complicated metaball models using only the built-in metaball functions above. +// However, you can create your own custom metaballs if desired. +// . // Each metaball is defined as a function of a 3-vector that gives the value of the metaball function // for that point in space. As is common in metaball implementations, we define the built-in metaballs using an -// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance from the -// metaball center. The spherical metaball therefore has a simple basic definition as `f(v) = 1/norm(v)`. -// With this framework, `f(v) >= c` defines a bounded object. Increasing the isovalue shrinks the -// object, and decreasing the isovalue grows the object. +// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance measured from +// the center or core of the metaball. The spherical metaball therefore has a simple basic definition as +// $f(v) = 1/\text{norm}(v)$. If we choose an isovalue $c$, then the set of points $v$ such that $f(v) >= c$ +// defines a bounded set — for example, a sphere with radius depending on the isovalue $c$. The +// default isovalue is $c=1$. Increasing the isovalue shrinks the object, and decreasing the isovalue grows +// the object. // . // To adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` // then the decay becomes $\frac{1}{d^{\frac 1 a}}$. This means, for example, that if you set influence to @@ -1334,7 +1428,11 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // . // You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) // that takes a single argument (a 3-vector) and returns a single numerical value. -// The returned value should define a function where in isovalue range [c,INF] defines a bounded object. See Example 19 for a demonstration of creating a custom metaball function. +// Generally, the function should return a scalar value that drops below the isovalue somewhere within your +// bounding box. If you want your custom metaball function to behave similar to to the built-in functions, +// the return value should fall off with distance as $1/d$. See Examples 20, 21, and 22 for demonstrations +// of creating custom metaball functions. Example 22 also shows how to make a metaball that works wtih +// `influence` and `cutoff`. // . // ***Voxel size and bounding box*** // . @@ -1342,14 +1440,13 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the // calculation and storage of 8,000,000 function values, and more processing and memory to generate // the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box -// requires only 8,000 function values and a modest computation time. A good rule is to keep the -// number of voxels below 10,000 for preview, and adjust the voxel size smaller for final -// rendering. A bounding box that is larger than your isosurface wastes time computing function -// values that are not needed. If the metaballs fit completely within the bounding box, you can -// call {{pointlist_bounds()}} on `vnf[0]` returned from the `metaballs()` function to get an -// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the -// same run time but increase the resolution. You can also set the parameter `show_stats=true` to -// get the bounds of the voxels containing the generated surfaces. +// requires only 8,000 function values and a modest computation time. A good rule is to keep the number +// of voxels below 10,000 for preview, and adjust the voxel size smaller for final rendering. Setting +// `voxel_size="auto"` sets a size such that approximately 8,000 voxels fit within your bounding box, +// which should be reasonable for initial preview. Because a bounding box that is too large wastes time +// computing function values that are not needed, you can also set the parameter `show_stats=true` to +// get the actual bounds of the voxels intersected by the surface. With this information, you may be +// able to decrease run time, or keep the same run time but increase the resolution. // . // The point list in the returned 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 @@ -1359,14 +1456,17 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // structure to {{vnf_unify_faces()}}. These steps can be computationally expensive // and are not normally necessary. // Arguments: -// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification. See above for more details, and see Example 21 for a demonstration. -// voxel_size = scalar size of the voxel cube that is used to sample the bounding box volume. -// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. +// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification. See above for more details, and see Example 23 for a demonstration. +// voxel_size = size of the voxel that is used to sample the bounding box volume. This can be "auto", a scalar size for a cubical voxel, or a 3-vector if you want non-cubical voxels. For "auto", the voxel size is set so that approximately `auto_voxels` (default 8000) quantity of voxels fit inside the bounding box. If you set `grow_bounds=false`, then bounding box is held fixed in size, and the voxel size is adjusted as needed so that whole voxels fit inside the bounding box. +// bounding_box = A designation of volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]` specifying the minimum and maximum box corner coordinates. With the default `grow_bounds=true`, the actual bounding box is enlarged if necessary to fit whole voxels, and centered around your requested box. // isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 // --- -// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face, possibly producing a non-manfold VNF that has holes. 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 possibly smaller bounding box to improve speed for subsequent renders. Enabling this parameter has a small speed penalty. Default: false -// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face, possibly producing non-manfold metaballs with holes where the bounding box intersects them. Default: true +// grow_bounds = When true, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. When false, shrinks voxels as needed to fit whole voxels inside the requested bounding box. Default: true +// show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of voxels that the surface passes through, and the number of triangles making up the surface, this is useful for getting information about a possibly smaller bounding box to improve speed for subsequent renders. Enabling this parameter has a small speed penalty. Default: false +// auto_voxels = Approximate quanity of voxels to have in the bounding box when `voxel_size="auto"`. Useful for fast preview of a reasonable number of voxels. Use with `show_stats=true` to see the corresponding voxel size. Default: 8000 +// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false // cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" // anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` // spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` @@ -1423,9 +1523,9 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // move([-10,0,17]), mb_torus(r_maj=6, r_min=2), // move([7,6,21])*xrot(90), mb_torus(r_maj=7, r_min=3) // ]; -// voxelsize = 0.5; +// voxel_size = 0.5; // boundingbox = [[-19,-9,9], [18,10,32]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,NoAxes,VPR=[75,0,20]): Two octahedrons interacting. // metaballs([ // move([-10,0,3]), mb_octahedron(8), @@ -1436,34 +1536,34 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // left(15), mb_sphere(10), // right(15), mb_sphere(10) // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-30,-19,-19], [30,19,19]]; -// metaballs(spec, voxelsize, boundingbox); -// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (which is the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. The right sphere is not abruptly cut off because the cutoff function is smooth and influence is normal. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself. +// metaballs(spec, voxel_size, boundingbox); +// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (5 units from the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. The right sphere is not abruptly cut off because the cutoff function is smooth and influence is normal. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself. // spec = [ // left(15), mb_sphere(10, cutoff=25), // right(15), mb_sphere(10) // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-30,-19,-19], [30,19,19]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,VPD=110): Here, the left sphere has less influence in addition to a cutoff. Setting `influence=0.5` results in a steeper falloff of contribution from the left sphere. Each sphere has a different size and shape due to unequal contributions based on distance. // spec = [ // left(15), mb_sphere(10, influence=0.5, cutoff=25), // right(15), mb_sphere(10) // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-30,-19,-19], [30,19,19]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,VPD=110): In this example, we have two size-10 spheres as before and one tiny sphere of 1.5 units radius offset a bit on the y axis. With an isovalue of 1, this figure would appear similar to Example 9 above, but here the isovalue has been set to 2, causing the surface to shrink around a smaller volume values greater than 2. Remember, higher isovalue thresholds cause metaballs to shrink. // spec = [ // left(15), mb_sphere(10), // right(15), mb_sphere(10), // fwd(15), mb_sphere(1.5) // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-30,-19,-19], [30,19,19]]; -// metaballs(spec, voxelsize, boundingbox, +// metaballs(spec, voxel_size, boundingbox, // isovalue=2); // Example(3D,VPD=110): Keeping `isovalue=2`, the influence of the tiny sphere has been set quite high, to 10. Notice that the tiny sphere shrinks a bit, but it has dramatically increased its contribution to its surroundings, causing the two other spheres to grow and meld into each other. The `influence` argument on a small metaball affects its surroundings more than itself. // spec = [ @@ -1471,10 +1571,17 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // move([15,0,0]), mb_sphere(10), // move([0,-15,0]), mb_sphere(1.5, influence=10) // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-30,-19,-19], [30,19,19]]; -// metaballs(spec, voxelsize, boundingbox, +// metaballs(spec, voxel_size, boundingbox, // isovalue=2); +// Example(3D,Med): Here is what happens when you set `influence` to less than 1. The only difference between these two spheres is influence. Both have `cutoff` set to prevent them from affecting each other. The sphere on the right has a low influence of 0.02, which translates to a falloff with distance $d$ proportional to $\frac{1}{d^50}$. That high exponent increases the *non-linear* nature of the function gradient at the isosurface, reducing the accuracy of the *linear* interpolation of where the the surface intersects each voxel, which causes ridges to appear. You could use this to create a texture deliberately (as with the trunk of the elephant in a later example), but it is usually better to use `cutoff` to limit the range of influence rather than reducing `influence` significantly below 1. +// spec = [ +// left(10), mb_sphere(8, cutoff=10, influence=1), +// right(10), mb_sphere(8, cutoff=10, influence=0.02) +// ]; +// bbox = [[-18,-8,-8], [18,8,8]]; +// metaballs(spec, voxel_size=0.4, bounding_box=bbox); // Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. // spec = [ // spheres of different sizes // move([-20,-20,-20]), mb_sphere(5), @@ -1483,38 +1590,38 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // move([0,0,20]), mb_sphere(5), // move([20,20,10]), mb_sphere(7) // ]; -// voxelsize = 1.5; +// voxel_size = 1.5; // boundingbox = [[-30,-31,-31], [32,31,31]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,NoAxes): A metaball can be negative. 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]]; // spec = [ // move(centers[0]), mb_sphere(8), // move(centers[1]), mb_sphere(3, negative=true) // ]; -// voxelsize = 0.25; +// voxel_size = 0.25; // isovalue = 1; // boundingbox = [[-7,-6,-6], [3,6,6]]; -// #metaballs(spec, voxelsize, boundingbox, isovalue); +// %metaballs(spec, voxel_size, boundingbox, isovalue); // color("green") move_copies(centers) sphere(d=1, $fn=16); -// Example(3D,VPD=105,VPT=[3,5,4.7]): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink, but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball as high influence, and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin. +// Example(3D,VPD=105,VPT=[3,5,4.7]): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink, but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball has high influence, and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin. // spec = [ // back(10), mb_sphere(20), // IDENT, mb_sphere(2, influence=30, // cutoff=20, negative=true), // ]; -// voxelsize = 0.5; +// voxel_size = 0.5; // boundingbox = [[-20,-4,-20], [20,30,20]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. // spec = [ // move([-7,-3,27])*zrot(55), mb_cuboid(6, squareness=1), // move([5,5,21]), mb_cuboid(5), // move([10,0,10]), mb_octahedron(5) // ]; -// voxelsize = 0.5; // a bit slow at this resolution +// voxel_size = 0.5; // a bit slow at this resolution // boundingbox = [[-12,-9,3], [18,10,32]]; -// metaballs(spec, voxelsize, boundingbox); +// metaballs(spec, voxel_size, boundingbox); // Example(3D,NoAxes,VPD=205,Med): A toy airplane, constructed only from metaball spheres with scaling. The bounding box is used to clip the wingtips, tail, and belly of the fuselage. // bounding_box = [[-55,-50,-5],[35,50,17]]; // spec = [ @@ -1524,22 +1631,69 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // move([-15,0,0])*scale([6,45,0.5]), mb_sphere(1) // wing // ]; // voxel_size = 1; -// metaballs(spec, voxel_size, bounding_box); -// Example(3D): Demonstration of a custom metaball function, in this case a sphere with some random noise added to its value. The `dv` argument must be first; it is calculated internally as a distance vector from the metaball center to a probe point inside the bounding box, and you convert it to a scalar distance `dist` that is calculated inside your function (`dist` could be a more complicated expression, depending on the shape of the metaball). The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to include the `cutoff` and `influence` arguments in a custom function, but this example shows how. -// function noisy_sphere(dv, r, noise_level, cutoff=INF, influence=1) = +// color("lightblue") metaballs(spec, voxel_size, bounding_box); +// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Custom metaballs are an advanced technique in which you define your own metaball shape by passing a function literal that takes a single argument: a coordinate in space relative to the metaball center called `point` here, but can be given any name. This distance vector from the origin is calculated internally and always passed to the function. Inside the function, it is converted to a scalar distance `dist`. The function literal expression sets all of your parameters. Only `point` is not set, and it becomes the single parameter to the function literal. The `spec` argument invokes your custom function as a function literal that passes `point` into it. +// function threelobe(point) = +// let( +// ang=atan2(point.y, point.x), +// r=norm([point.x,point.y])*(1.3+cos(3*ang)), +// dist=norm([point.z, r]) +// ) 3/dist; +// metaballs( +// spec = [ +// IDENT, function (point) threelobe(point), +// up(7), mb_sphere(r=4) +// ], +// voxel_size=0.5, +// bounding_box = [[-14,-12,-5],[8,12,13]]); +// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Here is a function nearly identical to the previous example, introducing additional dimensional parameters into the function to control its size and number of lobes. The bounding box size here is as small as possible for calculation efficiency, but if you expiriment with this using different argument values, you should increase the bounding box along with voxel size. +// function multilobe(point, size, lobes) = +// let( +// ang=atan2(point.y, point.x), +// r=norm([point.x,point.y])*(1.3+cos(lobes*ang)), +// dist=norm([point.z, r]) +// ) size/dist; +// metaballs( +// spec = [ +// left(7), +// function (point) multilobe(point, 3, 4), +// right(7)*zrot(60), +// function (point) multilobe(point, 3, 3) +// ], +// voxel_size=0.3, +// bounding_box = [[-16,-13,-5],[18,13,6]]); +// Example(3D): Next we show how to create a function that works like the built-ins. **This is a full-fledged implementation** that allows you to specify the function directly by name in the `spec` argument without needing the function literal syntax, and without needing the `point` argument in `spec`, as in the prior examples. You must define a calculation function that accepts the `point` position argument and then whatever other parameters your metaball uses (here `r` and `noise_level`). Then there is a "master" function that does some error checking and returns a function literal expression that sets all of your parameters. The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to use this implementation in your own custom functions; in fact it's easier simply to declare the function literal in your `spec` argument, but this example shows how to do it all. +// // +// // noisy sphere internal calculation function +// +// function noisy_sphere_calcs(point, r, noise_level, cutoff, exponent, neg) = // let( // noise = rands(0, noise_level, 1)[0], -// dist = norm(dv) + noise -// ) mb_cutoff(dist,cutoff) * (r/dist)^(1/influence); -// +// dist = norm(point) + noise +// ) neg * mb_cutoff(dist,cutoff) * (r/dist)^exponent; +// +// // noisy sphere "master" entry function to use in spec argument +// +// function noisy_sphere(r, noise_level, cutoff=INF, influence=1, negative=false, d) = +// assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") +// assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") +// let( +// r = get_radius(r=r,d=d), +// dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), +// neg = negative ? -1 : 1 +// ) // pass control as a function literal to the calc function +// function (point) noisy_sphere_calcs(point, r, noise_level, cutoff, 1/influence, neg); +// +// // define the scene and render it +// // spec = [ // left(9), mb_sphere(5), -// right(9), function (dv) noisy_sphere(dv, 5, 0.2), +// right(9), noisy_sphere(r=5, noise_level=0.2) // ]; -// voxelsize = 0.5; +// voxel_size = 0.5; // boundingbox = [[-16,-8,-8], [16,8,8]]; -// metaballs(spec, voxelsize, boundingbox); -// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A complex example using ellipsoids, a capsule, 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 clipping with the bottom of the bounding box. The center of the object is thick due to the contributions of three ellipsoids and a capsule converging. Designing an object like this using metaballs requires trial and error with low-resolution renders. +// metaballs(spec, voxel_size, boundingbox); +// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A more complex example using ellipsoids, a capsule, 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 clipping with the bottom of the bounding box. The center of the object is thick due to the contributions of three ellipsoids and a capsule converging. Designing an object like this using metaballs requires trial and error with low-resolution renders. // include // tetpts = zrot(15, p = 22 * regular_polyhedron_info("vertices", "tetrahedron")); // tettransform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; @@ -1554,10 +1708,10 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // // feet // for(i=[0:2]) each [move(2.2*tetpts[i]), mb_sphere(5, cutoff=30)], // ]; -// voxelsize = 1; +// voxel_size = 1; // boundingbox = [[-22,-32,-13], [36,32,46]]; // // useful to save as VNF for copies and manipulations -// vnf = metaballs(spec, voxelsize, boundingbox, isovalue=1); +// vnf = metaballs(spec, voxel_size, boundingbox, isovalue=1); // vnf_polyhedron(vnf); // Example(3D,Med,NoAxes,VPR=[70,0,30],VPD=520,VPT=[0,0,80]): This example demonstrates grouping metaballs together and nesting them in lists of other metaballs, to make a crude model of a hand. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight ellipsoid dent subtracted to hollow out the palm, to make the hand. // joints = [[0,0,1], [0,0,85], [0,-5,125], [0,-16,157], [0,-30,178]]; @@ -1568,7 +1722,7 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // thumb = [ // for(i=[0:2]) each [ // scale([1,1,1.2]), -// mb_connector(joints[i], joints[i+1], 9+i/2, influence=.28) +// mb_connector(joints[i], joints[i+1], 9+i/2, influence=0.28) // ] // ]; // allfingers = [ @@ -1584,20 +1738,46 @@ function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = // move([-10,-95,50])*yrot(10)*scale([2,2,0.95]), // mb_sphere(r=15, cutoff=50, influence=1.5, negative=true) // ]; -// voxsize=2.5; +// voxel_size=2.5; // bbox = [[-104,-40,-10], [79,18,188]]; -// metaballs(hand, voxsize, bbox, isovalue=1); +// metaballs(hand, voxel_size, bbox, isovalue=1); +// Example(3D,Med,NoAxes,VPR=[76,0,40],VPD=128,VPT=[4,-1,13]): A model of an elephant using cylinders, capsules, and disks. +// legD1 = 11; +// legD2 = 6; +// spec = [ +// // legs +// up(1)*fwd(8)*left(11), mb_cyl(d1=legD1, d2=legD2, h=22), +// up(1)*fwd(8)*right(10), mb_cyl(d1=legD1, d2=legD2, h=22), +// up(1)*back(8)*left(11), mb_cyl(d1=legD1, d2=legD2, h=22), +// up(1)*back(8)*right(10), mb_cyl(d1=legD1, d2=legD2, h=22), +// up(20)*yrot(90), mb_capsule(d=25, h=40, influence=0.5), // body +// right(21)*up(25)*yrot(-20), mb_capsule(r=7, h=25, influence=0.5, cutoff=9), // head +// right(24)*up(10)*yrot(15), mb_cyl(d1=3, d2=6, h=15, cutoff=2, influence=0.4), // trunk +// // ears +// right(18)*up(29)*fwd(11)*zrot(-20)*yrot(80)*scale([1.4,1,1]), mb_disk(r=5,h=2, cutoff=3), +// right(18)*up(29)*back(11)*zrot(20)*yrot(80)*scale([1.4,1,1]), mb_disk(r=5,h=2, cutoff=3), +// // tusks +// right(26)*up(13)*fwd(5)*yrot(135), mb_capsule(r=1, h=10, cutoff=1), +// right(26)*up(13)*back(5)*yrot(135), mb_capsule(r=1, h=10, cutoff=1) +// ]; +// bbox = [[-22,-17,-9], [31,17,38]]; +// metaballs(spec, voxel_size=1, bounding_box=bbox, isovalue=1); -module metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false) { - vnf = metaballs(spec, voxel_size, bounding_box, isovalue, closed, show_stats); +module metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, grow_bounds=true, auto_voxels=8000, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, show_box=false) { + vnf = metaballs(spec, voxel_size, bounding_box, isovalue, closed, grow_bounds, auto_voxels, show_stats); vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children(); + if(show_box) + #translate(bounding_box[0]) cube(bounding_box[1]-bounding_box[0]); } -function metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, show_stats=false) = +function metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, grow_bounds=true, auto_voxels=8000, show_stats=false) = assert(all_defined([spec, isovalue, bounding_box, voxel_size]), "\nThe parameters spec, isovalue, bounding_box, and voxel_size must all be defined.") + assert(voxel_size=="auto" || (is_finite(voxel_size) && voxel_size>0) || (is_vector(voxel_size) && all_positive(voxel_size)), "\nvoxel_size must be a positive number, a 3-vector of positive values, or \"auto\".") + assert(is_finite(isovalue) || (is_list(isovalue) && len(isovalue)==2 && is_num(isovalue[0]) && is_num(isovalue[1])), "\nIsovalue must be a number or a range; a number is the same as [number,INF].") assert(len(spec)%2==0, "\nThe spec parameter must be an even-length list of alternating transforms and functions") let( + isoval = is_list(isovalue) ? isovalue : [isovalue, INF], funclist = _mb_unwind_list(spec), nballs = len(funclist)/2, dummycheck = [ @@ -1612,28 +1792,31 @@ function metaballs(spec, voxel_size, bounding_box, isovalue=1, closed=true, show transpose(select(matrix_inverse(funclist[j]), 0,2)) ], - // new bounding box centered around original, forced to integer multiples of voxel size - halfvox = 0.5*voxel_size, - bbcenter = mean(bounding_box), - bbnums = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size), - newbbox = [bbcenter - halfvox*bbnums, bbcenter + halfvox*bbnums], + // new voxel or bounding box centered around original, to fit whole voxels + autovoxsize = voxel_size == "auto" ? _getautovoxsize(bounding_box, auto_voxels) : voxel_size, + voxsize = _getvoxsize(autovoxsize, bounding_box, grow_bounds), + bbox0 = is_num(bounding_box) + ? let(hb=0.5*bounding_box) _getbbox(autovoxsize, [[-hb,-hb,-hb],[hb,hb,hb]]) + : bounding_box, + newbbox = _getbbox(autovoxsize, bbox0, grow_bounds), // set up field array bot = newbbox[0], top = newbbox[1], + halfvox = 0.5*voxsize, // accumulate metaball contributions using matrices rather than sums - xset = [bot.x:voxel_size:top.x+halfvox], - yset = list([bot.y:voxel_size:top.y+halfvox]), - zset = list([bot.z:voxel_size:top.z+halfvox]), + xset = [bot.x:voxsize.x:top.x+halfvox.x], + yset = list([bot.y:voxsize.y:top.y+halfvox.y]), + zset = list([bot.z:voxsize.z:top.z+halfvox.z]), allpts = [for(x=xset, y=yset, z=zset) [x,y,z,1]], trans_pts = [for(i=[0:nballs-1]) allpts*transmatrix[i]], allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funclist[2*i+1](pt)]], //total = _sum(allvals,allvals[0]*EPSILON), total = _sum(slice(allvals,1,-1), allvals[0]), fieldarray = list_to_matrix(list_to_matrix(total,len(zset)),len(yset)) - ) isosurface(fieldarray, isovalue, voxel_size, closed=closed, show_stats=show_stats, _mb_origin=newbbox[0]); - + ) isosurface(fieldarray, isoval, voxsize, closed=closed, show_stats=show_stats, _mb_origin=newbbox[0]); +/// internal function: unwrap nested metaball specs in to a single list function _mb_unwind_list(list, parent_trans=[IDENT]) = let( dum1 = assert(is_list(list), "\nDid not find valid list of metaballs."), @@ -1662,67 +1845,82 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // SynTags: Geom,VNF // Topics: Isosurfaces, VNF Generators // Usage: As a module -// isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [show_stats=], ...) [ATTACHMENTS]; +// isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [grow_bounds=], [auto_voxels=], [show_stats=], ...) [ATTACHMENTS]; // Usage: As a function -// vnf = isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [show_stats=]); +// vnf = isosurface(f, isovalue, voxel_size, bounding_box, [reverse=], [closed=], [grow_bounds=], [auto_voxels=], [show_stats=]); // Description: -// Computes a [VNF structure](vnf.scad) of a 3D isosurface within a bounded box at a single -// isovalue or range of isovalues. +// Computes a [VNF structure](vnf.scad) of an object bounded by an isosurface or a range between two isosurfaces, within a specified bounding box. // The isosurface of a function $f(x,y,z)$ is the set of points where $f(x,y,z)=c$ for some -// constant isovalue, $c$. -// To provide a function you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) -// taking three parameters as input to define the grid coordinate location (e.g. `x,y,z`) and +// constant isovalue $c$. +// To provide a function, you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// taking an `[x,y,z]` coordinate as input to define the grid coordinate location and // returning a single numerical value. // You can also define an isosurface using a 3D array of values instead of a function, in which // case the isosurface is the set of points equal to the isovalue as interpolated from the array. // The array indices are in the order `[x][y][z]`. // . -// The VNF that is computed has the isosurface as its bounding surface, with all the points where -// $f(x,y,z)>c$ on the interior side of the surface. -// When the isovalue is a range, `[c1, c2]`, then the resulting VNF has two bounding surfaces -// corresponding to `c1` and `c2`, and the interior of the object are the points with intermediate -// isovalues; this generally produces a shell object that has an inside and outside surface. The -// range can start at `-INF` or end at `INF`. A single isovalue `c` is equivalent to `[c,INF]`. +// The specified isovalue must be a range $[c_1,c_2]$. The returned object is the set of points $p$ that +// satisfy $c_1 \leq f(p) \leq c_2$. If $f$ has values larger than $c_2$ and values smaller than $c_1$, +// then the result is a VNF with two bounding surfaces corresponding to the isosurfaces at $c_1$ and +// $c_2$. This is a shell object having two surfaces with a gap between them: the front faces of each +// surface face away from each other, and the backs face each other across the gap. If $f(p)c_1$ everywhere (which is true when +// $c_1 = -\infty$). Setting isovalue to `[-INF,c2]` or `[c1,INF]` always produces an object with a +// single bounding isosurface. To obtain a bounded object, think about whether the function values +// inside your object are smaller or larger than your isosurface value. If the values inside are +// smaller, you produce a bounded object using `[-INF,c]`. If the values inside are larger, you get a +// bounded object using `[c,INF]`. // . // The isosurface is evaluated over a bounding box defined by its minimum and maximum corners, // `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. This bounding box is divided into voxels of the // specified `voxel_size`. Smaller voxels produce a finer, smoother result at the expense of -// execution time. If the voxel size doesn't exactly divide your specified bounding box, then -// the bounding box is enlarged to contain whole voxels, and centered on your requested box. If +// execution time. By default, if the voxel size doesn't exactly divide your specified bounding box, then +// the bounding box is enlarged to contain whole voxels, and centered on your requested box (or, you can +// set `grow_bounds=false` to force the voxels to adjust in size to fit instead). If // the bounding box clips the isosurface and `closed=true` (the default), a surface is added to create // a closed manifold object. Setting `closed=false` causes the VNF to end at the bounding box, // resulting in a non-manifold shape that exposes the inside of the object. // . +// If your object is unbounded then when it intersects with the bounding box and `closed=true`, the +// result may appear like a solid cube, because the clipping faces are all you can see and the bounding +// surface is hidden inside. Setting `closed=false` removes the bounding box faces and exposes the +// inside structure. If you want the bounded object, you can correct this problem by changing the +// isovalue range: one of `[-INF,c2]` or `[c1,INF]` or `[c1,c2]` should do the job. +// . // The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. // A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the // calculation and storage of 8,000,000 function values, and more processing and memory to generate // the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box -// requires only 8,000 function values and a modest computation time. A good rule is to keep the -// number of voxels below 10,000 for preview, and adjust the voxel size smaller for final -// rendering. A bounding box that is larger than your isosurface wastes time computing function -// values that are not needed. If the isosurface fits completely within the bounding box, you can -// call {{pointlist_bounds()}} on `vnf[0]` returned from the `isosurface()` function to get an -// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the -// same run time but increase the resolution. You can also set the parameter `show_stats=true` to -// get the bounds of the voxels containing the surface. +// requires only 8,000 function values and a modest computation time. A good rule is to keep the number +// of voxels below 10,000 for preview, and adjust the voxel size smaller for final rendering. Setting +// `voxel_size="auto"` sets a size such that approximately 8,000 voxels fit within your bounding box, +// which should be reasonable for initial preview. Because a bounding box that is too large wastes time +// computing function values that are not needed, you can also set the parameter `show_stats=true` to +// get the actual bounds of the voxels intersected by the surface. With this information, you may be +// able to decrease run time, or keep the same run time but increase the resolution. // . -// 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 (often -// resulting from clipping by the bounding box) 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 computationally expensive +// 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 (often resulting from clipping by the bounding +// box) 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 computationally expensive // and are not normally necessary. // Arguments: // f = The isosurface function or array. -// isovalue = a scalar giving the isovalue parameter or a 2-vector giving an isovalue range. -// voxel_size = scalar size of the voxel cube that is used to sample the surface. -// bounding_box = When `f` is a function, a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. The actual bounding box enlarged if necessary to make the voxels fit perfectly, and centered around your requested box. When `f` is an array of values, `bounding_box` is already implied by the array size combined with `voxel_size`, in which case this implied bounding box is centered around the origin. +// isovalue = A 2-vector giving an isovalue range. For an unbounded range, use `[-INF, max_isovalue]` or `[min_isovalue, INF]`. +// voxel_size = size of the voxel that is used to sample the bounding box volume. This can be "auto", a scalar size for a cubical voxel, or a 3-vector if you want non-cubical voxels. For "auto", the voxel size is set so that approximately `auto_voxels` (default 8000) quantity of voxels fit inside the bounding box. If you set `grow_bounds=false`, then bounding box is held fixed in size, and the voxel size is adjusted as needed so that whole voxels fit inside the bounding box. +// bounding_box = A designation of volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin] [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. With the default `grow_bounds=true`, the actual bounding box is enlarged if necessary to fit whole voxels, and centered around your requested box. When `f` is an array of values, `bounding_box` is already implied by the array size combined with `voxel_size`, in which case this implied bounding box is centered around the origin. // --- // closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true // reverse = When true, reverses the orientation of the VNF faces. Default: false -// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that contain the surface, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false -// convexity = Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// grow_bounds = When true, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. When false, shrinks voxels as needed to fit whole voxels inside the requested bounding box. Default: true +// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that the surface passes through, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false +// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false +// auto_voxels = Approximate quanity of voxels to have in the bounding box when `voxel_size="auto"`. Useful for fast preview of a reasonable number of voxels. Use with `show_stats=true` to see the corresponding voxel size. Default: 8000 +// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 // cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" // anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` // spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` @@ -1733,63 +1931,98 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // "intersect" = Anchors to the surface of the shape. // Named Anchors: // "origin" = Anchor at the origin, oriented UP. -// 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 parameter 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*x, py = p*y, pz = p*z -// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); -// isovalue = 0; +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): These first three examples demonstrate the effect of isovalue range for the simplest of all surfaces: a sphere where $r=\sqrt{x^2+y^2+z^2}$, or `r = norm([x,y,z])` in OpenSCAD, in which the isosurface at `r` exists at every `[x,y,z]` point where the expression equals `r`. We use the isovalue range `[-INF,10]` here to make a sphere of radius 10, with a bounding box that cuts off half the sphere. The isovalue range could also be `[0,10]` because the minimum value of the expression is zero. +// isovalue = [-INF,10]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (xyz) norm(xyz), +// isovalue, voxel_size = 1, +// bounding_box = bbox); +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): In this second example, if we the isovalue to a range between 8 and 10, we get a shell with interior radius 8 and exterior radius 10. +// isovalue = [8,10]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (xyz) norm(xyz), +// isovalue, voxel_size = 1, +// bounding_box = bbox); +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): In this third example, we change the bounds so that the sphere radius is still 10 but the upper bound is infinity. Because the sphere expression `norm(xyz)` has larger values growing to infinity with distance from the origin, the resulting object appears as the bounding box with the minimum isovalue (the sphere) cut out of it. +// isovalue = [10,INF]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (xyz) norm(xyz), +// isovalue, voxel_size = 1, +// bounding_box = bbox); +// 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 is a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates using an additional parameter in the field function beyond just the `[x,y,z]` input; in this case to control the wavelength of the gyroid. +// function gyroid(xyz, wavelength) = let( +// p = 360/wavelength * xyz +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [0,INF]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isosurface(function (xyz) gyroid(xyz, wavelength=200), // isovalue, voxel_size=5, bounding_box=bbox, // 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*x, py = p*y, pz = p*z -// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); -// isovalue = 0; +// function gyroid(xyz, wavelength) = let( +// p = 360/wavelength * xyz +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [0,INF]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isosurface(function (xyz) gyroid(xyz, wavelength=200), // isovalue, voxel_size=5, bounding_box=bbox); // 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*x, py = p*y, pz = p*z -// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// function gyroid(xyz, wavelength) = let( +// p = 360/wavelength * xyz +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); // isovalue = [-0.3, 0.3]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isosurface(function (xyz) gyroid(xyz, wavelength=200), // isovalue, voxel_size=5, bounding_box=bbox, // 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*x, py = p*y, pz = p*z -// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// 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. +// function gyroid(xyz, wavelength) = let( +// p = 360/wavelength * xyz +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); // isovalue = [-0.3, 0.3]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isosurface(function (xyz) gyroid(xyz, wavelength=200), // isovalue, voxel_size=5, bounding_box=bbox); // Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). -// function schwartz_p(x,y,z, wavelength) = let( +// function schwartz_p(xyz, wavelength) = let( // p = 360/wavelength, -// px = p*x, py = p*y, pz = p*z +// px = p*xyz.x, py = p*xyz.y, pz = p*xyz.z // ) cos(px) + cos(py) + cos(pz); // isovalue = [-0.2, 0.2]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) schwartz_p(x,y,z, 100), +// isosurface(function (xyz) schwartz_p(xyz, 100), // isovalue, voxel_size=4, bounding_box=bbox); // Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). -// function neovius(x,y,z, wavelength) = let( +// function neovius(xyz, wavelength) = let( // p = 360/wavelength, -// px = p*x, py = p*y, pz = p*z +// px = p*xyz.x, py = p*xyz.y, pz = p*xyz.z // ) 3*(cos(px) + cos(py) + cos(pz)) + 4*cos(px)*cos(py)*cos(pz); // isovalue = [-0.3, 0.3]; // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(function (x,y,z) neovius(x,y,z,200), +// isosurface(function (xyz) neovius(xyz, 200), // isovalue, voxel_size=4, bounding_box=bbox); -// Example(3D): Using an array for the `f` argument instead of a function literal. +// Example(3D,NoAxes): Example of a bounded isosurface. +// isosurface( +// function (xyz) +// let(a=xyz_to_spherical(xyz), +// r=a[0], +// phi=a[1], +// theta=a[2] +// ) 1/(r*(3+cos(5*phi)+cos(4*theta))), +// isovalue = [0.1,INF], +// voxel_size = 0.25, +// bounding_box = [[-8,-7,-8],[6,7,8]] +// ); +// Example(3D,NoAxes): For shapes that occupy a cubical bounding box centered on the origin, you can simply specify a scalar for the size of the box. +// isosurface( +// function (p) (p.x*p.y*p.z^3 + 19*p.x^2*p.z^2)/norm(p)^2 + norm(p)^2, +// isovalue=[-INF,35], bounding_box=12, voxel_size=0.25); +// Example(3D,Med,NoAxes,VPD=165,VPR=[72,0,290],VPT=[0,0,0]): An object that could be a sort of support pillar. Here we set `show_box=true` to reveal that the bounding box is slightly bigger than it needs to be. The argument `show_stats=true` also outputs the voxel bounding box size as a suggestion of what it should be. +// isosurface( +// function (p) (p.x*p.y*p.z^3 - 3*p.x^2*p.z^2)/norm(p)^2 + norm(p)^2, +// isovalue=[-INF,35], bounding_box=[[-32,-32,-14],[32,32,14]], +// voxel_size = 0.8, show_box=true); +// Example(3D): Using an array for the `f` argument instead of a function literal. Each row of the array represents an X index for a YZ plane with the array Z indices changing fastest in each plane. The final object may need rotation to get the orientation you want. You don't pass the `bounding_box` argument here; it is implied by the array size and voxel size, and centered on the origin. // field = [ // repeat(0,[6,6]), // [ [0,1,2,2,1,0], @@ -1816,40 +2049,82 @@ function _mb_unwind_list(list, parent_trans=[IDENT]) = // repeat(0,[6,6]) // ]; // rotate([0,-90,180]) -// isosurface(field, isovalue=0.5, +// isosurface(field, isovalue=[0.5,INF], // voxel_size=10); -module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, _mb_origin=undef) { - vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, show_stats, _mb_origin); +module isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, grow_bounds=true, auto_voxels=8000, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, show_box=false, _mb_origin=undef) { + vnf = isosurface(f, isovalue, voxel_size, bounding_box, reverse, closed, grow_bounds, auto_voxels, show_stats, _mb_origin); vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) children(); + if(show_box) + %translate(bounding_box[0]) cube(bounding_box[1]-bounding_box[0]); } -function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, show_stats=false, _mb_origin=undef) = - assert(all_defined([f, isovalue, voxel_size]), "\nThe parameters f, isovalue, and bounding_box must all be defined.") +function isosurface(f, isovalue, voxel_size, bounding_box, reverse=false, closed=true, grow_bounds=true, auto_voxels=8000, show_stats=false, _mb_origin=undef) = + assert(all_defined([f, isovalue, voxel_size]), "\nThe parameters f, isovalue, and voxel_size must all be defined.") + assert(voxel_size=="auto" || (is_finite(voxel_size) && voxel_size>0) || (is_vector(voxel_size) && all_positive(voxel_size)), "\nvoxel_size must be a positive number, a 3-vector of positive values, or \"auto\".") + assert(is_list(isovalue) && len(isovalue)==2 && is_num(isovalue[0]) && is_num(isovalue[1]), "\nIsovalue must be a range; use [minvalue,INF] or [-INF,maxvalue] for an unbounded range.") assert((is_function(f) && is_def(bounding_box)) || (is_list(f) && is_undef(bounding_box)), "\nbounding_box must be passed if f is a function, and cannot be passed if f is an array.") + let( isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, isovalmax = is_list(isovalue) ? isovalue[1] : INF, - dum1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")), - hv = 0.5*voxel_size, - bbox = is_function(f) - ? let( // new bounding box quantized for voxel_size, centered around original box - bbcenter = mean(bounding_box), - bbn = v_ceil((bounding_box[1]-bounding_box[0]) / voxel_size) - ) [bbcenter - hv*bbn, bbcenter + hv*bbn] + dumiso1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")), + dumiso2 = assert(isovalmin != -INF || isovalmin != INF, "\nIsovalue range must be finite on one end."), + autovoxsize = voxel_size == "auto" ? _getautovoxsize(bounding_box, auto_voxels) : voxel_size, + voxsize = _getvoxsize(autovoxsize, bounding_box, grow_bounds), + bbox0 = is_num(bounding_box) + ? let(hb=0.5*bounding_box) _getbbox(autovoxsize, [[-hb,-hb,-hb],[hb,hb,hb]]) + : bounding_box, + bbox = is_function(f) ? _getbbox(autovoxsize, bbox0, grow_bounds) : let( // new bounding box, either centered on origin or using metaball origin + hv = 0.5*voxsize, dims = list_shape(f) - [1,1,1] ) is_def(_mb_origin) - ? [_mb_origin, _mb_origin+voxel_size*dims] // metaball bounding box - : [-hv*dims, hv*dims], // centered bounding box - cubes = _isosurface_cubes(voxel_size, bbox, + ? [_mb_origin, _mb_origin+v_mul(voxsize, dims)] // metaball bounding box + : let(corner=v_mul(hv,dims)) [-corner, corner], // centered bounding box + cubes = _isosurface_cubes(voxsize, bbox, fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, 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), - faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], - dum2 = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 + trianglepoints = _isosurface_triangles(cubes, voxsize, isovalmin, isovalmax, tritablemin, tritablemax), + faces = [ + for(i=[0:3:len(trianglepoints)-1]) + let(i1=i+1, i2=i+2) + if (norm(cross(trianglepoints[i1]-trianglepoints[i], + trianglepoints[i2]-trianglepoints[i])) > EPSILON) + [i,i1,i2] + ], + dum2 = show_stats ? _showstats_isosurface(voxsize, bbox, isovalue, cubes, trianglepoints, faces) : 0 ) [trianglepoints, faces]; + +// internal function: get "auto" voxel size given a desired number of voxels in a bounding box +function _getautovoxsize(bbox, numvoxels) = + let( + bbsiz = bbox[1]-bbox[0], + bbvol = bbsiz[0]*bbsiz[1]*bbsiz[2], + voxvol = bbvol/numvoxels + ) voxvol^(1/3); + +// internal function: get voxel size, adjusted if necessary to fit bounding box +function _getvoxsize(voxel_size, bounding_box, grow_bounds) = + let(voxsize0 = is_num(voxel_size) ? [voxel_size, voxel_size, voxel_size] : voxel_size) + grow_bounds ? voxsize0 // if grow_bounds==true, we don't adjust voxel size + : let( + reqboxsize = bounding_box[1] - bounding_box[0], + bbnums = v_ceil(v_div(bounding_box[1]-bounding_box[0], voxsize)), + newboxsize = v_mul(bbnums, voxsize0) + ) v_mul(voxsize0, v_div(reqboxsize, newboxsize)); + +/// internal function: get bounding box, adjusted in size and centered on requested box +function _getbbox(voxel_size, bounding_box, grow_bounds) = + let(voxsize0 = is_num(voxel_size) ? [voxel_size, voxel_size, voxel_size] : voxel_size) + grow_bounds ? + let( // adjust bounding box + bbcenter = mean(bounding_box), + bbnums = v_ceil(v_div(bounding_box[1]-bounding_box[0], voxsize0)), + halfbb = 0.5 * v_mul(voxsize0, bbnums) + ) [bbcenter - halfbb, bbcenter + halfbb] + : bounding_box; // if grow_bounds==false, we don't adjust bounding box From 72753493f55df5f52537b84313e17ab11ee3441f Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 26 Feb 2025 11:39:29 -0800 Subject: [PATCH 10/11] See Also updates Make dovetail() findable from partitions.scad, and partitions findable from joiners.scad --- joiners.scad | 2 +- partitions.scad | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/joiners.scad b/joiners.scad index bf0e6509..39ef69e5 100644 --- a/joiners.scad +++ b/joiners.scad @@ -556,7 +556,7 @@ module joiner(l=40, w=10, base=10, ang=30, screwsize, anchor=CENTER, spin=0, ori // Synopsis: Creates a possibly tapered dovetail shape. // SynTags: Geom // Topics: Joiners, Parts -// See Also: joiner(), snap_pin(), rabbit_clip() +// See Also: joiner(), snap_pin(), rabbit_clip(), partition(), partition_mask(), partition_cut_mask() // // Usage: // dovetail(gender, w=|width, h=|height, slide|thickness=, [slope=|angle=], [taper=|back_width=], [chamfer=], [r=|radius=], [round=], [extra=], [$slop=]) diff --git a/partitions.scad b/partitions.scad index f06f037e..a704903a 100644 --- a/partitions.scad +++ b/partitions.scad @@ -430,7 +430,7 @@ function _partition_cutpath(l, h, cutsize, cutpath, gap) = // Synopsis: Creates a mask to remove half an object with the remaining half suitable for reassembly. // SynTags: Geom // Topics: Partitions, Masking, Paths -// See Also: partition_cut_mask(), partition() +// See Also: partition_cut_mask(), partition(), dovetail() // Usage: // partition_mask(l, w, h, [cutsize], [cutpath], [gap], [inverse], [$slop=], [anchor=], [spin=], [orient=]) [ATTACHMENTS]; // Description: @@ -485,7 +485,7 @@ module partition_mask(l=100, w=100, h=100, cutsize=10, cutpath="jigsaw", gap=0, // Synopsis: Creates a mask to cut an object into two subparts that can be reassembled. // SynTags: Geom // Topics: Partitions, Masking, Paths -// See Also: partition_mask(), partition() +// See Also: partition_mask(), partition(), dovetail() // Usage: // partition_cut_mask(l, [cutsize], [cutpath], [gap], [inverse], [$slop=], [anchor=], [spin=], [orient=]) [ATTACHMENTS]; // Description: @@ -531,7 +531,7 @@ module partition_cut_mask(l=100, h=100, cutsize=10, cutpath="jigsaw", gap=0, anc // Synopsis: Cuts an object in two with matched joining edges, then separates the parts. // SynTags: Geom, VNF, Path, Region // Topics: Partitions, Masking, Paths -// See Also: partition_cut_mask(), partition_mask() +// See Also: partition_cut_mask(), partition_mask(), dovetail() // Usage: // partition(size, [spread], [cutsize], [cutpath], [gap], [spin], [$slop=]) CHILDREN; // Description: From be62fd30af8413581b9b5d9e1ef80f191a1cc66d Mon Sep 17 00:00:00 2001 From: Richard Milewski Date: Wed, 26 Feb 2025 11:48:32 -0800 Subject: [PATCH 11/11] Create isosurface.scad --- isosurface.scad | 2313 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2313 insertions(+) create mode 100644 isosurface.scad diff --git a/isosurface.scad b/isosurface.scad new file mode 100644 index 00000000..0073a26f --- /dev/null +++ b/isosurface.scad @@ -0,0 +1,2313 @@ +///////////////////////////////////////////////////////////////////// +// LibFile: isosurface.scad +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), +// are bounded and closed organic surfaces that smoothly blend together. +// Metaballs are a specific kind of [isosurface](https://en.wikipedia.org/wiki/Isosurface). +// . +// An isosurface, or implicit surface, is a three-dimensional surface representing all points of a +// constant 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 shapes. For example, spherical metaballs can be formulated using a set of point +// centers that define the metaballs locations. For metaballs, a function is defined for +// all points in a 3D volume based on the distance from any point to the centers of each metaball. The +// combined contributions from all the metaballs results in a function that varies in a complicated +// way throughout the volume. When two metaballs are far apart, they appear simply as spheres, but when +// they are close together they enlarge, reach toward each other, and meld together in a smooth +// fashion. The resulting metaball model appears as smoothly blended blobby shapes. The +// implementation below provides metaballs of a variety of types including spheres, cuboids, and +// cylinders (cones), with optional parameters to adjust the influence of one metaball on others, +// and the cutoff distance where the metaball's influence stops. +// . +// In general, an isosurface can be defined using any function of three variables $x, y, z$. +// 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$. Such a function is also known as an "implied surface" because the function *implies* a +// surface of constant value within a volume of space. The constant $c$ is referred to as the "isovalue". +// Changing the isovalue changes the position of the isosurface, depending on how the function is +// defined. Because metaballs are isosurfaces, they also have an isovalue. The isovalue is also known +// as the "threshold". +// . +// Some isosurface functions are unbounded, extending infinitely in all directions. A familiar example may +// be a [gryoid](https://en.wikipedia.org/wiki/Gyroid), which is often used as a volume infill pattern in +// [fused filament fabrication](https://en.wikipedia.org/wiki/Fused_filament_fabrication). The gyroid +// isosurface is unbounded and periodic in all three dimensions. +// . +// This file provides modules and functions to create a [VNF](vnf.scad) using metaballs, or from general isosurfaces. +// . +// The point list in the generated VNF structure contains many duplicated points. This is normally not a +// problem for rendering the shape, but machine roundoff differences may result in Manifold issuing +// warnings when doing the final render, causing rendering to abort if you have enabled the "stop on +// first warning" setting. You can prevent this by passing the VNF through {{vnf_quantize()}} using a +// quantization of 1e-7, or you can pass the VNF structure into {{vnf_merge_points()}}, which also +// removes the duplicates. Additionally, flat surfaces (often resulting from clipping by the bounding +// box) 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 computationally expensive +// and are not normally necessary. +// Includes: +// include +// include +// FileGroup: Advanced Modeling +// FileSummary: Isosurfaces and metaballs. +////////////////////////////////////////////////////////////////////// + + +/* +Lookup Tables for Transvoxel's Modified Marching Cubes + +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, a cube with corners 2, 3, and 7 greater than the threshold isovalue would have the index 10000110, an 8-bit binary number with bits 2, 3, and 7 set to 1, corresponding to decimal index 134. After determining the cube's index value this way, the triangulation order is looked up in a table. + +Axes are + z + (top) + | y (back) + | / + |/ + +----- x (right) + +Vertex and edge layout (heavier = and # indicate closer to viewer): + + 3 +----------+ 7 +----10----+ + /: /| /: /| + / : / | 1 2 5 6 + 1 +==========+5 | +=====9====+ | + # 2+ - - - # -+ 6 # +- - 11-# -+ + # / # / 0 3 4 7 + #/ #/ #/ #/ + 0 +==========+ 4 +=====8=====+ + +z changes fastest, then y, then x. +*/ + +/// Pair of vertex indices for each edge on the voxel +_MCEdgeVertexIndices = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [3, 7], + [2, 6] +]; + +/// 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], + [1,0,9], + [9,1,8,8,1,3], + [3,2,11], + [2,11,0,0,11,8], + [1,0,9,3,2,11], + [11,1,2,11,9,1,11,8,9], + [10,2,1], + [2,1,10,0,3,8], + [0,9,2,2,9,10], + [8,2,3,8,10,2,8,9,10], + [1,10,3,3,10,11], + [10,0,1,10,8,0,10,11,8], + [9,3,0,9,11,3,9,10,11], + [9,10,8,8,10,11], + [7,4,8], + [0,3,4,4,3,7], + [0,9,1,4,8,7], + [1,4,9,1,7,4,1,3,7], + [11,3,2,8,7,4], + [4,11,7,4,2,11,4,0,2], + [3,2,11,0,9,1,4,8,7], + [9,1,4,4,1,7,7,1,2,7,2,11], + [7,4,8,1,10,2], + [7,4,3,3,4,0,10,2,1], + [10,2,9,9,2,0,7,4,8], + [7,4,9,7,9,2,9,10,2,3,7,2], + [1,10,3,3,10,11,4,8,7], + [4,0,7,0,1,10,7,0,10,7,10,11], + [7,4,8,9,3,0,9,11,3,9,10,11], + [7,4,11,4,9,11,9,10,11], + [5,9,4], + [8,0,3,9,4,5], + [1,0,5,5,0,4], + [5,8,4,5,3,8,5,1,3], + [3,2,11,5,9,4], + [2,11,0,0,11,8,5,9,4], + [4,5,0,0,5,1,11,3,2], + [11,8,2,8,4,5,2,8,5,2,5,1], + [5,9,4,1,10,2], + [0,3,8,1,10,2,5,9,4], + [2,5,10,2,4,5,2,0,4], + [4,5,8,8,5,3,3,5,10,3,10,2], + [11,3,10,10,3,1,4,5,9], + [4,5,9,10,0,1,10,8,0,10,11,8], + [4,5,10,4,10,3,10,11,3,0,4,3], + [4,5,8,5,10,8,10,11,8], + [5,9,7,7,9,8], + [3,9,0,3,5,9,3,7,5], + [7,0,8,7,1,0,7,5,1], + [3,7,1,1,7,5], + [5,9,7,7,9,8,2,11,3], + [5,9,0,5,0,11,0,2,11,7,5,11], + [2,11,3,7,0,8,7,1,0,7,5,1], + [2,11,1,11,7,1,7,5,1], + [8,7,9,9,7,5,2,1,10], + [10,2,1,3,9,0,3,5,9,3,7,5], + [2,0,10,0,8,7,10,0,7,10,7,5], + [10,2,5,2,3,5,3,7,5], + [5,9,8,5,8,7,1,10,3,10,11,3], + [1,10,0,0,10,11,0,11,7,0,7,5,0,5,9], + [8,7,0,0,7,5,0,5,10,0,10,11,0,11,3], + [5,11,7,10,11,5], + [11,6,7], + [3,8,0,7,11,6], + [1,0,9,7,11,6], + [9,1,8,8,1,3,6,7,11], + [6,7,2,2,7,3], + [0,7,8,0,6,7,0,2,6], + [6,7,2,2,7,3,9,1,0], + [9,1,2,9,2,7,2,6,7,8,9,7], + [10,2,1,11,6,7], + [2,1,10,3,8,0,7,11,6], + [0,9,2,2,9,10,7,11,6], + [6,7,11,8,2,3,8,10,2,8,9,10], + [7,10,6,7,1,10,7,3,1], + [1,10,0,0,10,8,8,10,6,8,6,7], + [9,10,0,10,6,7,0,10,7,0,7,3], + [6,7,10,7,8,10,8,9,10], + [4,8,6,6,8,11], + [6,3,11,6,0,3,6,4,0], + [11,6,8,8,6,4,1,0,9], + [6,4,11,4,9,1,11,4,1,11,1,3], + [2,8,3,2,4,8,2,6,4], + [0,2,4,4,2,6], + [9,1,0,2,8,3,2,4,8,2,6,4], + [9,1,4,1,2,4,2,6,4], + [4,8,6,6,8,11,1,10,2], + [1,10,2,6,3,11,6,0,3,6,4,0], + [0,9,10,0,10,2,4,8,6,8,11,6], + [11,6,3,3,6,4,3,4,9,3,9,10,3,10,2], + [1,10,6,1,6,8,6,4,8,3,1,8], + [1,10,0,10,6,0,6,4,0], + [0,9,3,3,9,10,3,10,6,3,6,4,3,4,8], + [4,10,6,9,10,4], + [4,5,9,6,7,11], + [7,11,6,8,0,3,9,4,5], + [1,0,5,5,0,4,11,6,7], + [11,6,7,5,8,4,5,3,8,5,1,3], + [3,2,7,7,2,6,9,4,5], + [5,9,4,0,7,8,0,6,7,0,2,6], + [1,0,4,1,4,5,3,2,7,2,6,7], + [4,5,8,8,5,1,8,1,2,8,2,6,8,6,7], + [6,7,11,5,9,4,1,10,2], + [5,9,4,7,11,6,0,3,8,2,1,10], + [7,11,6,2,5,10,2,4,5,2,0,4], + [6,7,11,3,8,4,3,4,5,3,5,2,2,5,10], + [9,4,5,7,10,6,7,1,10,7,3,1], + [5,9,4,8,0,1,8,1,10,8,10,7,7,10,6], + [6,7,10,10,7,3,10,3,0,10,0,4,10,4,5], + [4,5,8,8,5,10,8,10,6,8,6,7], + [9,6,5,9,11,6,9,8,11], + [0,3,9,9,3,5,5,3,11,5,11,6], + [1,0,8,1,8,6,8,11,6,5,1,6], + [11,6,3,6,5,3,5,1,3], + [2,6,3,6,5,9,3,6,9,3,9,8], + [5,9,6,9,0,6,0,2,6], + [3,2,8,8,2,6,8,6,5,8,5,1,8,1,0], + [1,6,5,2,6,1], + [2,1,10,9,6,5,9,11,6,9,8,11], + [2,1,10,5,9,0,5,0,3,5,3,6,6,3,11], + [10,2,5,5,2,0,5,0,8,5,8,11,5,11,6], + [10,2,5,5,2,3,5,3,11,5,11,6], + [5,9,6,6,9,8,6,8,3,6,3,1,6,1,10], + [5,9,6,6,9,0,6,0,1,6,1,10], + [8,3,0,5,10,6], + [6,5,10], + [6,10,5], + [3,8,0,5,6,10], + [9,1,0,10,5,6], + [3,8,1,1,8,9,6,10,5], + [6,10,5,2,11,3], + [8,0,11,11,0,2,5,6,10], + [10,5,6,1,0,9,3,2,11], + [5,6,10,11,1,2,11,9,1,11,8,9], + [2,1,6,6,1,5], + [5,6,1,1,6,2,8,0,3], + [6,9,5,6,0,9,6,2,0], + [8,9,3,9,5,6,3,9,6,3,6,2], + [3,6,11,3,5,6,3,1,5], + [5,6,11,5,11,0,11,8,0,1,5,0], + [0,9,3,3,9,11,11,9,5,11,5,6], + [5,6,9,6,11,9,11,8,9], + [7,4,8,5,6,10], + [0,3,4,4,3,7,10,5,6], + [4,8,7,9,1,0,10,5,6], + [6,10,5,1,4,9,1,7,4,1,3,7], + [11,3,2,7,4,8,5,6,10], + [10,5,6,4,11,7,4,2,11,4,0,2], + [7,4,8,3,2,11,9,1,0,10,5,6], + [10,5,6,7,4,9,7,9,1,7,1,11,11,1,2], + [2,1,6,6,1,5,8,7,4], + [7,4,0,7,0,3,5,6,1,6,2,1], + [8,7,4,6,9,5,6,0,9,6,2,0], + [5,6,9,9,6,2,9,2,3,9,3,7,9,7,4], + [4,8,7,3,6,11,3,5,6,3,1,5], + [7,4,11,11,4,0,11,0,1,11,1,5,11,5,6], + [4,8,7,11,3,0,11,0,9,11,9,6,6,9,5], + [5,6,9,9,6,11,9,11,7,9,7,4], + [9,4,10,10,4,6], + [6,10,4,4,10,9,3,8,0], + [0,10,1,0,6,10,0,4,6], + [3,8,4,3,4,10,4,6,10,1,3,10], + [9,4,10,10,4,6,3,2,11], + [8,0,2,8,2,11,9,4,10,4,6,10], + [11,3,2,0,10,1,0,6,10,0,4,6], + [2,11,1,1,11,8,1,8,4,1,4,6,1,6,10], + [4,1,9,4,2,1,4,6,2], + [3,8,0,4,1,9,4,2,1,4,6,2], + [4,6,0,0,6,2], + [3,8,2,8,4,2,4,6,2], + [3,1,11,1,9,4,11,1,4,11,4,6], + [9,4,1,1,4,6,1,6,11,1,11,8,1,8,0], + [11,3,6,3,0,6,0,4,6], + [8,6,11,4,6,8], + [10,7,6,10,8,7,10,9,8], + [10,9,6,9,0,3,6,9,3,6,3,7], + [8,7,0,0,7,1,1,7,6,1,6,10], + [6,10,7,10,1,7,1,3,7], + [3,2,11,10,7,6,10,8,7,10,9,8], + [6,10,7,7,10,9,7,9,0,7,0,2,7,2,11], + [11,3,2,1,0,8,1,8,7,1,7,10,10,7,6], + [6,10,7,7,10,1,7,1,2,7,2,11], + [8,7,6,8,6,1,6,2,1,9,8,1], + [0,3,9,9,3,7,9,7,6,9,6,2,9,2,1], + [8,7,0,7,6,0,6,2,0], + [7,2,3,6,2,7], + [11,3,6,6,3,1,6,1,9,6,9,8,6,8,7], + [11,7,6,1,9,0], + [11,3,6,6,3,0,6,0,8,6,8,7], + [11,7,6], + [10,5,11,11,5,7], + [10,5,11,11,5,7,0,3,8], + [7,11,5,5,11,10,0,9,1], + [3,8,9,3,9,1,7,11,5,11,10,5], + [5,2,10,5,3,2,5,7,3], + [0,2,8,2,10,5,8,2,5,8,5,7], + [0,9,1,5,2,10,5,3,2,5,7,3], + [10,5,2,2,5,7,2,7,8,2,8,9,2,9,1], + [1,11,2,1,7,11,1,5,7], + [8,0,3,1,11,2,1,7,11,1,5,7], + [0,9,5,0,5,11,5,7,11,2,0,11], + [3,8,2,2,8,9,2,9,5,2,5,7,2,7,11], + [5,7,1,1,7,3], + [8,0,7,0,1,7,1,5,7], + [0,9,3,9,5,3,5,7,3], + [9,7,8,5,7,9], + [8,5,4,8,10,5,8,11,10], + [10,5,4,10,4,3,4,0,3,11,10,3], + [1,0,9,8,5,4,8,10,5,8,11,10], + [9,1,4,4,1,3,4,3,11,4,11,10,4,10,5], + [10,5,2,2,5,3,3,5,4,3,4,8], + [10,5,2,5,4,2,4,0,2], + [9,1,0,3,2,10,3,10,5,3,5,8,8,5,4], + [10,5,2,2,5,4,2,4,9,2,9,1], + [1,5,2,5,4,8,2,5,8,2,8,11], + [2,1,11,11,1,5,11,5,4,11,4,0,11,0,3], + [4,8,5,5,8,11,5,11,2,5,2,0,5,0,9], + [5,4,9,2,3,11], + [4,8,5,8,3,5,3,1,5], + [0,5,4,1,5,0], + [0,9,3,3,9,5,3,5,4,3,4,8], + [5,4,9], + [11,4,7,11,9,4,11,10,9], + [0,3,8,11,4,7,11,9,4,11,10,9], + [0,4,1,4,7,11,1,4,11,1,11,10], + [7,11,4,4,11,10,4,10,1,4,1,3,4,3,8], + [9,4,7,9,7,2,7,3,2,10,9,2], + [8,0,7,7,0,2,7,2,10,7,10,9,7,9,4], + [1,0,10,10,0,4,10,4,7,10,7,3,10,3,2], + [7,8,4,10,1,2], + [9,4,1,1,4,2,2,4,7,2,7,11], + [8,0,3,2,1,9,2,9,4,2,4,11,11,4,7], + [7,11,4,11,2,4,2,0,4], + [3,8,2,2,8,4,2,4,7,2,7,11], + [9,4,1,4,7,1,7,3,1], + [9,4,1,1,4,7,1,7,8,1,8,0], + [3,4,7,0,4,3], + [7,8,4], + [8,11,9,9,11,10], + [0,3,9,3,11,9,11,10,9], + [1,0,10,0,8,10,8,11,10], + [10,3,11,1,3,10], + [3,2,8,2,10,8,10,9,8], + [9,2,10,0,2,9], + [1,0,10,10,0,8,10,8,3,10,3,2], + [2,10,1], + [2,1,11,1,9,11,9,8,11], + [2,1,11,11,1,9,11,9,0,11,0,3], + [11,0,8,2,0,11], + [3,11,2], + [1,8,3,9,8,1], + [1,9,0], + [8,3,0], + [] +]; + +/// Same list as above, but with each row in reverse order. Needed for generating shells (two isosurfaces at slightly different iso values). +/// It is more efficient to have this static table than to call reverse() repeatedly while triangulating (although this static table was generated that way). +_MCTriangleTable_reverse = [ + [], + [0,8,3], + [9,0,1], + [3,1,8,8,1,9], + [11,2,3], + [8,11,0,0,11,2], + [11,2,3,9,0,1], + [9,8,11,1,9,11,2,1,11], + [1,2,10], + [8,3,0,10,1,2], + [10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8], + [11,10,3,3,10,1], + [8,11,10,0,8,10,1,0,10], + [11,10,9,3,11,9,0,3,9], + [11,10,8,8,10,9], + [8,4,7], + [7,3,4,4,3,0], + [7,8,4,1,9,0], + [7,3,1,4,7,1,9,4,1], + [4,7,8,2,3,11], + [2,0,4,11,2,4,7,11,4], + [7,8,4,1,9,0,11,2,3], + [11,2,7,2,1,7,7,1,4,4,1,9], + [2,10,1,8,4,7], + [1,2,10,0,4,3,3,4,7], + [8,4,7,0,2,9,9,2,10], + [2,7,3,2,10,9,2,9,7,9,4,7], + [7,8,4,11,10,3,3,10,1], + [11,10,7,10,0,7,10,1,0,7,0,4], + [11,10,9,3,11,9,0,3,9,8,4,7], + [11,10,9,11,9,4,11,4,7], + [4,9,5], + [5,4,9,3,0,8], + [4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5], + [4,9,5,11,2,3], + [4,9,5,8,11,0,0,11,2], + [2,3,11,1,5,0,0,5,4], + [1,5,2,5,8,2,5,4,8,2,8,11], + [2,10,1,4,9,5], + [4,9,5,2,10,1,8,3,0], + [4,0,2,5,4,2,10,5,2], + [2,10,3,10,5,3,3,5,8,8,5,4], + [9,5,4,1,3,10,10,3,11], + [8,11,10,0,8,10,1,0,10,9,5,4], + [3,4,0,3,11,10,3,10,4,10,5,4], + [8,11,10,8,10,5,8,5,4], + [8,9,7,7,9,5], + [5,7,3,9,5,3,0,9,3], + [1,5,7,0,1,7,8,0,7], + [5,7,1,1,7,3], + [3,11,2,8,9,7,7,9,5], + [11,5,7,11,2,0,11,0,5,0,9,5], + [1,5,7,0,1,7,8,0,7,3,11,2], + [1,5,7,1,7,11,1,11,2], + [10,1,2,5,7,9,9,7,8], + [5,7,3,9,5,3,0,9,3,1,2,10], + [5,7,10,7,0,10,7,8,0,10,0,2], + [5,7,3,5,3,2,5,2,10], + [3,11,10,3,10,1,7,8,5,8,9,5], + [9,5,0,5,7,0,7,11,0,11,10,0,0,10,1], + [3,11,0,11,10,0,10,5,0,5,7,0,0,7,8], + [5,11,10,7,11,5], + [7,6,11], + [6,11,7,0,8,3], + [6,11,7,9,0,1], + [11,7,6,3,1,8,8,1,9], + [3,7,2,2,7,6], + [6,2,0,7,6,0,8,7,0], + [0,1,9,3,7,2,2,7,6], + [7,9,8,7,6,2,7,2,9,2,1,9], + [7,6,11,1,2,10], + [6,11,7,0,8,3,10,1,2], + [6,11,7,10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8,11,7,6], + [1,3,7,10,1,7,6,10,7], + [7,6,8,6,10,8,8,10,0,0,10,1], + [3,7,0,7,10,0,7,6,10,0,10,9], + [10,9,8,10,8,7,10,7,6], + [11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6], + [9,0,1,4,6,8,8,6,11], + [3,1,11,1,4,11,1,9,4,11,4,6], + [4,6,2,8,4,2,3,8,2], + [6,2,4,4,2,0], + [4,6,2,8,4,2,3,8,2,0,1,9], + [4,6,2,4,2,1,4,1,9], + [2,10,1,11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6,2,10,1], + [6,11,8,6,8,4,2,10,0,10,9,0], + [2,10,3,10,9,3,9,4,3,4,6,3,3,6,11], + [8,1,3,8,4,6,8,6,1,6,10,1], + [0,4,6,0,6,10,0,10,1], + [8,4,3,4,6,3,6,10,3,10,9,3,3,9,0], + [4,10,9,6,10,4], + [11,7,6,9,5,4], + [5,4,9,3,0,8,6,11,7], + [7,6,11,4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5,7,6,11], + [5,4,9,6,2,7,7,2,3], + [6,2,0,7,6,0,8,7,0,4,9,5], + [7,6,2,7,2,3,5,4,1,4,0,1], + [7,6,8,6,2,8,2,1,8,1,5,8,8,5,4], + [2,10,1,4,9,5,11,7,6], + [10,1,2,8,3,0,6,11,7,4,9,5], + [4,0,2,5,4,2,10,5,2,6,11,7], + [10,5,2,2,5,3,5,4,3,4,8,3,11,7,6], + [1,3,7,10,1,7,6,10,7,5,4,9], + [6,10,7,7,10,8,10,1,8,1,0,8,4,9,5], + [5,4,10,4,0,10,0,3,10,3,7,10,10,7,6], + [7,6,8,6,10,8,10,5,8,8,5,4], + [11,8,9,6,11,9,5,6,9], + [6,11,5,11,3,5,5,3,9,9,3,0], + [6,1,5,6,11,8,6,8,1,8,0,1], + [3,1,5,3,5,6,3,6,11], + [8,9,3,9,6,3,9,5,6,3,6,2], + [6,2,0,6,0,9,6,9,5], + [0,1,8,1,5,8,5,6,8,6,2,8,8,2,3], + [1,6,2,5,6,1], + [11,8,9,6,11,9,5,6,9,10,1,2], + [11,3,6,6,3,5,3,0,5,0,9,5,10,1,2], + [6,11,5,11,8,5,8,0,5,0,2,5,5,2,10], + [6,11,5,11,3,5,3,2,5,5,2,10], + [10,1,6,1,3,6,3,8,6,8,9,6,6,9,5], + [10,1,6,1,0,6,0,9,6,6,9,5], + [6,10,5,0,3,8], + [10,5,6], + [5,10,6], + [10,6,5,0,8,3], + [6,5,10,0,1,9], + [5,10,6,9,8,1,1,8,3], + [3,11,2,5,10,6], + [10,6,5,2,0,11,11,0,8], + [11,2,3,9,0,1,6,5,10], + [9,8,11,1,9,11,2,1,11,10,6,5], + [5,1,6,6,1,2], + [3,0,8,2,6,1,1,6,5], + [0,2,6,9,0,6,5,9,6], + [2,6,3,6,9,3,6,5,9,3,9,8], + [5,1,3,6,5,3,11,6,3], + [0,5,1,0,8,11,0,11,5,11,6,5], + [6,5,11,5,9,11,11,9,3,3,9,0], + [9,8,11,9,11,6,9,6,5], + [10,6,5,8,4,7], + [6,5,10,7,3,4,4,3,0], + [6,5,10,0,1,9,7,8,4], + [7,3,1,4,7,1,9,4,1,5,10,6], + [10,6,5,8,4,7,2,3,11], + [2,0,4,11,2,4,7,11,4,6,5,10], + [6,5,10,0,1,9,11,2,3,8,4,7], + [2,1,11,11,1,7,1,9,7,9,4,7,6,5,10], + [4,7,8,5,1,6,6,1,2], + [1,2,6,1,6,5,3,0,7,0,4,7], + [0,2,6,9,0,6,5,9,6,4,7,8], + [4,7,9,7,3,9,3,2,9,2,6,9,9,6,5], + [5,1,3,6,5,3,11,6,3,7,8,4], + [6,5,11,5,1,11,1,0,11,0,4,11,11,4,7], + [5,9,6,6,9,11,9,0,11,0,3,11,7,8,4], + [4,7,9,7,11,9,11,6,9,9,6,5], + [6,4,10,10,4,9], + [0,8,3,9,10,4,4,10,6], + [6,4,0,10,6,0,1,10,0], + [10,3,1,10,6,4,10,4,3,4,8,3], + [11,2,3,6,4,10,10,4,9], + [10,6,4,10,4,9,11,2,8,2,0,8], + [6,4,0,10,6,0,1,10,0,2,3,11], + [10,6,1,6,4,1,4,8,1,8,11,1,1,11,2], + [2,6,4,1,2,4,9,1,4], + [2,6,4,1,2,4,9,1,4,0,8,3], + [2,6,0,0,6,4], + [2,6,4,2,4,8,2,8,3], + [6,4,11,4,1,11,4,9,1,11,1,3], + [0,8,1,8,11,1,11,6,1,6,4,1,1,4,9], + [6,4,0,6,0,3,6,3,11], + [8,6,4,11,6,8], + [8,9,10,7,8,10,6,7,10], + [7,3,6,3,9,6,3,0,9,6,9,10], + [10,6,1,6,7,1,1,7,0,0,7,8], + [7,3,1,7,1,10,7,10,6], + [8,9,10,7,8,10,6,7,10,11,2,3], + [11,2,7,2,0,7,0,9,7,9,10,7,7,10,6], + [6,7,10,10,7,1,7,8,1,8,0,1,2,3,11], + [11,2,7,2,1,7,1,10,7,7,10,6], + [1,8,9,1,2,6,1,6,8,6,7,8], + [1,2,9,2,6,9,6,7,9,7,3,9,9,3,0], + [0,2,6,0,6,7,0,7,8], + [7,2,6,3,2,7], + [7,8,6,8,9,6,9,1,6,1,3,6,6,3,11], + [0,9,1,6,7,11], + [7,8,6,8,0,6,0,3,6,6,3,11], + [6,7,11], + [7,5,11,11,5,10], + [8,3,0,7,5,11,11,5,10], + [1,9,0,10,11,5,5,11,7], + [5,10,11,5,11,7,1,9,3,9,8,3], + [3,7,5,2,3,5,10,2,5], + [7,5,8,5,2,8,5,10,2,8,2,0], + [3,7,5,2,3,5,10,2,5,1,9,0], + [1,9,2,9,8,2,8,7,2,7,5,2,2,5,10], + [7,5,1,11,7,1,2,11,1], + [7,5,1,11,7,1,2,11,1,3,0,8], + [11,0,2,11,7,5,11,5,0,5,9,0], + [11,7,2,7,5,2,5,9,2,9,8,2,2,8,3], + [3,7,1,1,7,5], + [7,5,1,7,1,0,7,0,8], + [3,7,5,3,5,9,3,9,0], + [9,7,5,8,7,9], + [10,11,8,5,10,8,4,5,8], + [3,10,11,3,0,4,3,4,10,4,5,10], + [10,11,8,5,10,8,4,5,8,9,0,1], + [5,10,4,10,11,4,11,3,4,3,1,4,4,1,9], + [8,4,3,4,5,3,3,5,2,2,5,10], + [2,0,4,2,4,5,2,5,10], + [4,5,8,8,5,3,5,10,3,10,2,3,0,1,9], + [1,9,2,9,4,2,4,5,2,2,5,10], + [11,8,2,8,5,2,8,4,5,2,5,1], + [3,0,11,0,4,11,4,5,11,5,1,11,11,1,2], + [9,0,5,0,2,5,2,11,5,11,8,5,5,8,4], + [11,3,2,9,4,5], + [5,1,3,5,3,8,5,8,4], + [0,5,1,4,5,0], + [8,4,3,4,5,3,5,9,3,3,9,0], + [9,4,5], + [9,10,11,4,9,11,7,4,11], + [9,10,11,4,9,11,7,4,11,8,3,0], + [10,11,1,11,4,1,11,7,4,1,4,0], + [8,3,4,3,1,4,1,10,4,10,11,4,4,11,7], + [2,9,10,2,3,7,2,7,9,7,4,9], + [4,9,7,9,10,7,10,2,7,2,0,7,7,0,8], + [2,3,10,3,7,10,7,4,10,4,0,10,10,0,1], + [2,1,10,4,8,7], + [11,7,2,7,4,2,2,4,1,1,4,9], + [7,4,11,11,4,2,4,9,2,9,1,2,3,0,8], + [4,0,2,4,2,11,4,11,7], + [11,7,2,7,4,2,4,8,2,2,8,3], + [1,3,7,1,7,4,1,4,9], + [0,8,1,8,7,1,7,4,1,1,4,9], + [3,4,0,7,4,3], + [4,8,7], + [10,11,9,9,11,8], + [9,10,11,9,11,3,9,3,0], + [10,11,8,10,8,0,10,0,1], + [10,3,1,11,3,10], + [8,9,10,8,10,2,8,2,3], + [9,2,0,10,2,9], + [2,3,10,3,8,10,8,0,10,10,0,1], + [1,10,2], + [11,8,9,11,9,1,11,1,2], + [3,0,11,0,9,11,9,1,11,11,1,2], + [11,0,2,8,0,11], + [2,11,3], + [1,8,9,3,8,1], + [0,9,1], + [0,3,8], + [] +]; + + +/// _cubindex() - private function, called by _isosurface_cubes() +/// Return the index ID of a voxel depending on the field strength at each corner exceeding isoval. +function _cubeindex(f, isoval) = + (f[0] >= isoval ? 1 : 0) + + (f[1] >= isoval ? 2 : 0) + + (f[2] >= isoval ? 4 : 0) + + (f[3] >= isoval ? 8 : 0) + + (f[4] >= isoval ? 16 : 0) + + (f[5] >= isoval ? 32 : 0) + + (f[6] >= isoval ? 64 : 0) + + (f[7] >= isoval ? 128 : 0); + +/* +----------------------------------------------------------- +Bounding box clipping support: + +Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. + + 4(back) + 3 +----------+ 7 + /: 5(top) /| + / : / | + 1 +==========+5 | <-- 3(side) +0(side) --> # 2+ - - - # -+ 6 + # / # / + #/ 2(bot) #/ + 0 +----------+ 4 + 1(front) + +The clip face uses different indexing. After vertex coordinates and function values are assigned to each corner from the original voxel based on _MCFaceVertexIndices below, this is the clip face diagram: + +(1) (2) + +----1----+ + | | + 0 2 + | | + +----3----+ +(0) (3) +*/ + +/// four indices for each face of the cube, counterclockwise looking from inside out +_MCFaceVertexIndices = [ + [], + [0,2,3,1], // left, x=0 plane + [0,1,5,4], // front, y=0 plane + [0,4,6,2], // bottom, z=0 plane + [4,5,7,6], // right, x=voxsize plane + [2,6,7,3], // back, y=voxsize plane + [1,3,7,5], // top, z=voxsize plane +]; + +/* +/// Pair of vertex indices for each edge on the clip face (using clip face indexing) +_MCClipEdgeVertexIndices = [ + [0,1], [1,2], [2,3], [3,0] +]; + +/// For each of the 16 configurations of a clip face, define a list of triangles, specified as pairs of corner ID and edge ID arrays, with a total of 3 points in each pair. Each pair has the form [corner],[edge1,edge2] or [corner1,corner2],[edge]. +/// In keeping with the convention for triangulating an isosurface through a voxel, analogous to the case in which two surfaces separate two diagonally opposite greater-than-isovalue corners of one face, in 2D contour terms it is assumed there is a valley separating two diagonally-opposite high corners, not a ridge connecting them. The two triangulation cases for opposing corners are set up accordingly. +_MCClipTriangleTable = [ + [], // 0 - 0000 - ignored + [[0],[0,3]], // 1 - 0001 + [[1],[1,0]], // 2 - 0010 + [[0,1],[1], [0],[1,3]], // 3 - 0011 + [[2],[2,1]], // 4 - 0100 + [[0],[0,3], [2],[2,1]], // 5 - 0101 - opposing corners + [[1,2],[0], [2],[2,0]], // 6 - 0110 + [[0,1],[3], [1],[2,3], [1,2],[2]], // 7 - 0111 + [[3],[3,2]], // 8 - 1000 + [[3,0],[0], [3],[0,2]], // 9 - 1001 + [[1],[1,0], [3],[3,2]], //10 - 1010 - opposing corners + [[0,1],[1], [0],[1,2], [3,0],[2]], //11 - 1011 + [[2,3],[3], [2],[3,1]], //12 - 1100 + [[3,0],[0], [3],[0,1], [2,3],[1]], //13 - 1101 + [[2,3],[3], [2],[3,0], [1,2],[0]], //14 - 1110 + [[0,1,2],[], [0,2,3],[]], //15 - 1111 +]; + +/// _clipfacindex() - private function, called by _clipfacevertices() +/// Return the index ID of a voxel face depending on the field strength at each corner exceeding isoval. +function _clipfacindex(f, isoval) = + (f[0] > isoval ? 1 : 0) + + (f[1] > isoval ? 2 : 0) + + (f[2] > isoval ? 4 : 0) + + (f[3] > isoval ? 8 : 0); +*/ + +/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. voxsize is a 3-vector. +function _bbox_faces(v0, voxsize, bbox) = let( + a = v_abs(v0-bbox[0]), + bb1 = bbox[1] - voxsize, + b = v0-bb1 +) [ + if(a[0]=-EPSILON) 4, + if(b[1]>=-EPSILON) 5, + if(b[2]>=-EPSILON) 6 +]; +/// End of bounding-box face-clipping stuff +/// ----------------------------------------------------------- + + +/// isosurface_cubes() - private function, called by isosurface() +/// This implements a marching cubes algorithm, sacrificing some memory in favor of speed. +/// Return a list of voxel cube structures that have one or both surfaces isovalmin or isovalmax intersecting them, and cubes inside the isosurface volume that are at the bounds of the bounding box. +/// The cube structure is: +/// [cubecoord, cubeindex_isomin, cubeindex_isomax, cf, bfaces] +/// where +/// cubecoord is the [x,y,z] coordinate of the front left bottom corner of the voxel. +/// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections. +/// cf (corner function) is vector containing the 8 field strength values at each corner of the voxel cube. +/// 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, and `voxsize` is a 3-vector. + +function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isovalmax, closed=true) = let( + // get field intensities + field = is_def(fieldarray) + ? fieldarray + : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+hv) [ + for(x=[v.x:voxsize.x:b1.x]) [ + for(y=[v.y:voxsize.y:b1.y]) [ + for(z=[v.z:voxsize.z:b1.z]) + fieldfunc(x,y,z) + ] + ] + ], + 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]+i*voxsize.x) + for(j=[0:ny]) let(y=v0[1]+j*voxsize.y) + for(k=[0:nz]) let(z=v0[2]+k*voxsize.z) + let(i1=i+1, j1=j+1, k1=k+1, + cf = [ // cube corner field values clamped to ±1e9 + min(1e9,max(-1e9,field[i][j][k])), + min(1e9,max(-1e9,field[i][j][k1])), + min(1e9,max(-1e9,field[i][j1][k])), + min(1e9,max(-1e9,field[i][j1][k1])), + min(1e9,max(-1e9,field[i1][j][k])), + min(1e9,max(-1e9,field[i1][j][k1])), + min(1e9,max(-1e9,field[i1][j1][k])), + min(1e9,max(-1e9,field[i1][j1][k1])) + ], + mincf = min(cf), + maxcf = max(cf), + cubecoord = [x,y,z], + bfaces = closed ? _bbox_faces(cubecoord, voxsize, bbox) : [], + cubefound_isomin = (mincf<=isovalmin && isovalmin<=maxcf), + cubefound_isomax = (mincf<=isovalmax && isovalmax<=maxcf), + cubefound_outer = len(bfaces)==0 ? false + : let( + bf = flatten([for(i=bfaces) _MCFaceVertexIndices[i]]), + sumcond = len([for(b=bf) if(isovalmin<=cf[b] && cf[b]<=isovalmax) 1 ]) + ) sumcond == len(bf), // true if full faces are inside + cubeindex_isomin = cubefound_isomin ? _cubeindex(cf, isovalmin) : 0, + cubeindex_isomax = cubefound_isomax ? _cubeindex(cf, isovalmax) : 0 + ) if(cubefound_isomin || cubefound_isomax || cubefound_outer) + [ // return data structure: + cubecoord, // voxel lower coordinate + cubeindex_isomin, // cube ID for isomin + cubeindex_isomax, // cube ID for isomax + cf, // clamped voxel corner values + bfaces // list of bounding box faces, if any + ] +]; + + +/// _isosurface_trangles() - called by isosurface() +/// Given a list of voxel cubes structures, triangulate the isosurface(s) that intersect each cube and return a list of triangle vertices. +function _isosurface_triangles(cubelist, voxsize, isovalmin, isovalmax, tritablemin, tritablemax) = [ + for(cl=cubelist) + let( + v = cl[0], // voxel coord + cbidxmin = cl[1], // cube ID for isomvalmin + cbidxmax = cl[2], // cube ID for isovalmax + f = cl[3], // function values for each cube corner + bbfaces = cl[4], // faces (if any) on the bounding box + vcube = [ // list of cube corner vertex coordinates + v, v+[0,0,voxsize.z], v+[0,voxsize.y,0], v+[0,voxsize.y,voxsize.z], + v+[voxsize.x,0,0], v+[voxsize.x,0,voxsize.z], + v+[voxsize.x,voxsize.y,0], v+voxsize + ] + ) + each [ + if(len(tritablemin[cbidxmin])>0) for(ei=tritablemin[cbidxmin]) // min surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(len(tritablemax[cbidxmax])>0) for(ei=tritablemax[cbidxmax]) // max surface + let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(len(bbfaces)>0) for(bf = bbfaces) + each _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) + ] +]; + + +/* +/// Generate triangles for the special case of voxel faces clipped by the bounding box +/// (more efficient than _bbfacevertices below but doesn't work with isovalue ranges) +function _clipfacevertices(vcube, f, bbface, isovalmax, isovalmin) = + let( + vi = _MCFaceVertexIndices[bbface], // four voxel face vertex indices + vfc = [ for(i=vi) vcube[i] ], // four voxel face vertex coordinates + fld = [ for(i=vi) f[i] ], // four corner field values + minidx = _clipfacindex(fld, isovalmin), + maxidx = _clipfacindex(fld, isovalmax) + ) [ + if(minidx>0) + let(tabl = _MCClipTriangleTable[minidx]) + for(i=[0:2:len(tabl)-1]) each [ + for(c=tabl[i]) vfc[c], + for(ei=tabl[i+1]) let( + edge = _MCClipEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = fld[vi1] - fld[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-fld[vi0]) / denom + ) vfc[vi0] + u*(vfc[vi1]-vfc[vi0]) + ], + if(false && maxidx>0) + let(tabl = _MCClipTriangleTable[maxidx]) + for(i=[0:2:len(tabl)-1]) each [ + for(c=tabl[i]) vfc[c], + for(ei=tabl[i+1]) let( + edge = _MCClipEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = fld[vi1] - fld[vi0], + u = abs(denom)<0.00001 ? 0.5 : (isovalmin-fld[vi0]) / denom + ) vfc[vi0] + u*(vfc[vi1]-vfc[vi0]) + ] + ]; +*/ + +/// Generate triangles for the special case of voxel faces clipped by the bounding box +/// TODO: Address isolated manifold error in edge case where two different isosurfaces intersect the same voxel AND that voxel is on a box boundary. This can be contrived but hasn't yet come up in actual testing. +function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( + vi = _MCFaceVertexIndices[bbface], // four voxel face vertex indices + //vfc = [ for(i=vi) vcube[i] ], // four voxel face vertex coordinates + //fld = [ for(i=vi) f[i] ], // four corner field values + pgon = flatten([ + for(i=[0:3]) let( // for each line segment... + vi0=vi[i], // voxel corner 0 index + vi1=vi[(i+1)%4], // voxel corner 1 index + f0 = f[vi0], // field value at corner 0 + f1 = f[vi1], // field value at corner 1 + fmin = min(f0, f1), // min field of the corners + fmax = max(f0, f1), // max field of the corners + ilowbetween = (fmin < isovalmin && isovalmin < fmax), + ihighbetween = (fmin < isovalmax && isovalmax < fmax), + denom = f1-f0 + ) [ // traverse the edge, output vertices as they are found + if(isovalmin <= f0 && f0 <= isovalmax)// && abs(f1-f0)>0.001) + // vertex 0 is on or between min and max isovalues + //echo(vfc, fld) + vcube[vi0], + // for f0f1) + let(u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f0)/denom) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(ilowbetween && f0>f1) + let(u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f0)/denom) + vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + ] + ]), + npgon = len(pgon), + triangles = npgon<3 ? [] : [ + for(i=[1:len(pgon)-2]) [pgon[0], pgon[i], pgon[i+1]] + ]) flatten(triangles); + + + +/// ---------- metaball stuff starts here ---------- + +/// Animated metaball demo made with BOSL2 here: https://imgur.com/a/m29q8Qd + +/// Built-in metaball functions corresponding to each MB_ index. +/// For speed, they are split into four functions, each handling a different combination of influence != 1 or influence == 1, and cutoff < INF or cutoff == INF. + +/// public metaball cutoff function if anyone wants it (demonstrated in example) + +function mb_cutoff(dist, cutoff) = dist>=cutoff ? 0 : 0.5*(cos(180*(dist/cutoff)^4)+1); + + +/// metaball sphere + +function _mb_sphere_basic(point, r, neg) = neg*r/norm(point); +function _mb_sphere_influence(point, r, ex, neg) = neg * (r/norm(point))^ex; +function _mb_sphere_cutoff(point, r, cutoff, neg) = let(dist=norm(point)) + neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_sphere_full(point, r, cutoff, ex, neg) = let(dist=norm(point)) + neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_sphere(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(point) _mb_sphere_basic(point,r,neg) + : !is_finite(cutoff) ? function (point) _mb_sphere_influence(point,r,1/influence, neg) + : influence==1 ? function (point) _mb_sphere_cutoff(point,r,cutoff,neg) + : function (point) _mb_sphere_full(point,r,cutoff,1/influence,neg); + + +/// metaball rounded cube + +function _mb_cuboid_basic(point, inv_size, xp, neg) = + let( + point=inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + : (abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) + ) neg/dist; +function _mb_cuboid_influence(point, inv_size, xp, ex, neg) = let( + point = inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + :(abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) +) neg / dist^ex; +function _mb_cuboid_cutoff(point, inv_size, xp, cutoff, neg) = let( + point = inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + : (abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist; +function _mb_cuboid_full(point, inv_size, xp, ex, cutoff, neg) = let( + point = inv_size * point, + dist = xp >= 1100 ? max(v_abs(point)) + :(abs(point.x)^xp + abs(point.y)^xp + abs(point.z)^xp) ^ (1/xp) +) neg * mb_cutoff(dist, cutoff) / dist^ex; + +function mb_cuboid(size, squareness=0.5, cutoff=INF, influence=1, negative=false) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + assert((is_finite(size) && size>0) || (is_vector(size) && all_positive(size)), "\nsize must be a positive number or a 3-vector of positive values.") + let( + xp = _squircle_se_exponent(squareness), + neg = negative ? -1 : 1, + inv_size = is_num(size) ? 2/size + : [[2/size.x,0,0],[0,2/size.y,0],[0,0,2/size.z]] + ) + !is_finite(cutoff) && influence==1 ? function(point) _mb_cuboid_basic(point, inv_size, xp, neg) + : !is_finite(cutoff) ? function(point) _mb_cuboid_influence(point, inv_size, xp, 1/influence, neg) + : influence==1 ? function(point) _mb_cuboid_cutoff(point, inv_size, xp, cutoff, neg) + : function (point) _mb_cuboid_full(point, inv_size, xp, 1/influence, cutoff, neg); + + +/// metaball rounded cylinder / cone + +function _revsurf_basic(point, path, coef, neg, maxdist) = + let( + pt = [norm([point.x,point.y]), point.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside = [] == [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ? -1 : 1 + ) + neg * coef / (inside*dist+maxdist); + +function _revsurf_influence(point, path, coef, exp, neg, maxdist) = + let( + pt = [norm([point.x,point.y]), point.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside = [] == [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ? -1 : 1 + ) + neg * (coef / (inside*dist+maxdist))^exp; + +function _revsurf_cutoff(point, path, coef, cutoff, neg, maxdist) = + let( + pt = [norm([point.x,point.y]), point.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside = [] == [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ? -1 : 1, + d=inside*dist+maxdist + ) + neg * mb_cutoff(d, cutoff) * coef/d; + +function _revsurf_full(point, path, coef, cutoff, exp, neg, maxdist) = + let( + pt = [norm([point.x,point.y]), point.z], + segs = pair(path), + dist = min([for(seg=segs) + let( + c=seg[1]-seg[0], + s0 = seg[0]-pt, + t = -s0*c/(c*c) + ) + t<0 ? norm(s0) + : t>1 ? norm(seg[1]-pt) + : norm(s0+t*c)]), + inside = [] == [for(seg=segs) + if (cross(seg[1]-seg[0], pt-seg[0]) > EPSILON) 1] + ? -1 : 1, + d=inside*dist+maxdist + ) + neg * mb_cutoff(d, cutoff) * (coef/d)^exp; + +function mb_cyl(h,r,rounding=0,r1,r2,l,height,length,d1,d2,d, cutoff=INF, influence=1, negative=false) = + let( + r1 = get_radius(r1=r1,r=r, d1=d1, d=d), + r2 = get_radius(r1=r2,r=r, d1=d2, d=d), + h = first_defined([h,l,height,length],"h,l,height,length") + ) + assert(all_positive([influence]), "influence must be a positive number") + assert(is_finite(rounding) && rounding>=0, "rounding must be a nonnegative number") + assert(is_finite(r1) && r1>0, "r/r1/d/d1 must be a positive number") + assert(is_finite(r2) && r2>0, "r/r2/d/d2 must be a positive number") + assert(is_num(cutoff) && cutoff>0, "cutoff must be a positive number") + let( + vang = atan2(r1-r2,h), + facelen = adj_ang_to_hyp(h, abs(vang)), + roundlen1 = rounding/tan(45-vang/2), + roundlen2 = rounding/tan(45+vang/2), + sides = [[0,h/2], [r2,h/2], [r1,-h/2], [0,-h/2]], + neg = negative ? -1 : 1 + ) + assert(roundlen1 <= r1, "size of rounding is larger than the r1 radius of the cylinder/cone") + assert(roundlen2 <= r2, "size of rounding is larger than the r2 radius of the cylinder/cone") + assert(roundlen1+roundlen2 < facelen, "Roundings don't fit on the edge length of the cylinder/cone") + let( + shifted = offset(sides, delta=-rounding, closed=false, check_valid=false), + bisect1 = [shifted[1],unit(shifted[0]-shifted[1])+unit(shifted[2]-shifted[1])+shifted[1]], + bisect2 = [shifted[2],unit(shifted[3]-shifted[2])+unit(shifted[1]-shifted[2])+shifted[2]], + side_isect = line_intersection(bisect1,bisect2), + top_isect = line_intersection(bisect1,[[0,0],[0,1]]), + bot_isect = line_intersection(bisect2,[[0,0],[0,1]]), + maxdist = side_isect.x>0 ?point_line_distance(side_isect, select(shifted,1,2)) + : max(point_line_distance(top_isect, select(shifted,1,2)), + point_line_distance(bot_isect, select(shifted,1,2))) + ) + !is_finite(cutoff) && influence==1 ? function(point) _revsurf_basic(point, shifted, maxdist+rounding, neg, maxdist) + : !is_finite(cutoff) ? function(point) _revsurf_influence(point, shifted, maxdist+rounding, 1/influence, neg, maxdist) + : influence==1 ? function(point) _revsurf_cutoff(point, shifted, maxdist+rounding, cutoff, neg, maxdist) + : function (point) _revsurf_full(point, shifted, maxdist+rounding, cutoff, 1/influence, neg, maxdist); + + +/// metaball disk with rounded edge + +function _mb_disk_basic(point, hl, r, neg) = + let( + rdist=norm([point.x,point.y]), + dist = rdist0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + h2 = h/2, + or = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && or>0, "\ninvalid radius or diameter."), + r = or - h2, + dum3 = assert(r>0, "\nDiameter must be greater than height."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(point) _mb_disk_basic(point,h2,r,neg) + : !is_finite(cutoff) ? function(point) _mb_disk_influence(point,h2,r,1/influence, neg) + : influence==1 ? function(point) _mb_disk_cutoff(point,h2,r,cutoff,neg) + : function (point) _mb_disk_full(point, h2, r, cutoff, 1/influence, neg); + + +/// metaball capsule (round-ended cylinder) + +function _mb_capsule_basic(dv, hl, r, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +) neg*r/dist; +function _mb_capsule_influence(dv, hl, r, ex, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z<=hl ? norm([dv.x,dv.y]) : norm(dv-[0,0,hl]) +) neg * (r/dist)^ex; +function _mb_capsule_cutoff(dv, hl, r, cutoff, neg) = let( + dist = dv.z<-hl ? norm(dv-[0,0,-hl]) + : dv.z0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + h = one_defined([h,l,height,length],"h,l,height,length"), + dum1 = assert(is_finite(h) && h>0, "\ncylinder height must be a positive number."), + r = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + sh = h-2*r, // straight side length + dum3 = assert(sh>0, "\nTotal length must accommodate rounded ends of cylinder."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(dv) _mb_capsule_basic(dv,sh/2,r,neg) + : !is_finite(cutoff) ? function(dv) _mb_capsule_influence(dv,sh/2,r,1/influence, neg) + : influence==1 ? function(dv) _mb_capsule_cutoff(dv,sh/2,r,cutoff,neg) + : function (dv) _mb_capsule_full(dv, sh/2, r, cutoff, 1/influence, neg); + + +/// metaball connector cylinder - calls mb_capsule* functions after transform + +function mb_connector(p1, p2, r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + dum1 = assert(is_vector(p1,3), "\nConnector start point p1 must be a 3D coordinate.") + assert(is_vector(p2,3), "\nConnector end point p2 must be a 3D coordinate.") + assert(p1 != p2, "\nStart and end points p1 and p2 cannot be the same."), + r = get_radius(r=r,d=d), + dum2 = assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1, + dc = p2-p1, // center-to-center distance + midpt = reverse(-0.5*(p1+p2)), + h = norm(dc)/2, // center-to-center length (cylinder height) + transform = submatrix(down(h)*rot(from=dc,to=UP)*move(-p1) ,[0:2], [0:3]) + ) + !is_finite(cutoff) && influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_basic(newdv,h,r,neg) + : !is_finite(cutoff) ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_influence(newdv,h,r,1/influence, neg) + : influence==1 ? function(dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_cutoff(newdv,h,r,cutoff,neg) + : function (dv) + let(newdv = transform * [each dv,1]) + _mb_capsule_full(newdv, h, r, cutoff, 1/influence, neg); + + +/// metaball torus + +function _mb_torus_basic(point, rmaj, rmin, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg*rmin/dist; +function _mb_torus_influence(point, rmaj, rmin, ex, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) neg * (rmin/dist)^ex; +function _mb_torus_cutoff(point, rmaj, rmin, cutoff, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) + neg * mb_cutoff(dist, cutoff) * rmin/dist; +function _mb_torus_full(point, rmaj, rmin, cutoff, ex, neg) = + let(dist = norm([norm([point.x,point.y])-rmaj, point.z])) + neg * mb_cutoff(dist, cutoff) * (rmin/dist)^ex; + +function mb_torus(r_maj, r_min, cutoff=INF, influence=1, negative=false, d_maj, d_min, or,od,ir,id) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") + let( + _or = get_radius(r=or, d=od, dflt=undef), + _ir = get_radius(r=ir, d=id, dflt=undef), + _r_maj = get_radius(r=r_maj, d=d_maj, dflt=undef), + _r_min = get_radius(r=r_min, d=d_min, dflt=undef), + r_maj = is_finite(_r_maj)? _r_maj : + is_finite(_ir) && is_finite(_or)? (_or + _ir)/2 : + is_finite(_ir) && is_finite(_r_min)? (_ir + _r_min) : + is_finite(_or) && is_finite(_r_min)? (_or - _r_min) : + assert(false, "Bad major size parameter."), + r_min = is_finite(_r_min)? _r_min : + is_finite(_ir)? (maj_rad - _ir) : + is_finite(_or)? (_or - maj_rad) : + assert(false, "\nBad minor size parameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(point) _mb_torus_basic(point, r_maj, r_min, neg) + : !is_finite(cutoff) ? function(point) _mb_torus_influence(point, r_maj, r_min, 1/influence, neg) + : influence==1 ? function(point) _mb_torus_cutoff(point, r_maj, r_min, cutoff, neg) + : function(point) _mb_torus_full(point, r_maj, r_min, cutoff, 1/influence, neg); + + +/// metaball octahedron + +function _mb_octahedron_basic(point, r, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg*r/dist; +function _mb_octahedron_influence(point, r, ex, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * (r/dist)^ex; +function _mb_octahedron_cutoff(point, r, cutoff, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * mb_cutoff(dist, cutoff) * r/dist; +function _mb_octahedron_full(point, r, cutoff, ex, neg) = + let(dist = abs(point.x) + abs(point.y) + abs(point.z)) neg * mb_cutoff(dist, cutoff) * (r/dist)^ex; + +function mb_octahedron(r, cutoff=INF, influence=1, negative=false, d) = + assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") + assert(is_finite(influence) && is_num(influence) && influence>0, "\ninfluence must be a positive number.") + let( + r = get_radius(r=r,d=d), + dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), + neg = negative ? -1 : 1 + ) + !is_finite(cutoff) && influence==1 ? function(point) _mb_octahedron_basic(point,r,neg) + : !is_finite(cutoff) ? function(point) _mb_octahedron_influence(point,r,1/influence, neg) + : influence==1 ? function(point) _mb_octahedron_cutoff(point,r,cutoff,neg) + : function(point) _mb_octahedron_full(point,r,cutoff,1/influence,neg); + + +// Function&Module: metaballs() +// Synopsis: Creates a group of 3D metaballs (smoothly connected blobs). +// SynTags: Geom,VNF +// Topics: Metaballs, Isosurfaces, VNF Generators +// See Also: isosurface() +// Usage: As a module +// metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [exact_bounds=], [convexity=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = metaballs(spec, voxel_size, bounding_box, [isovalue=], [closed=], [exact_bounds=], [convexity=], [show_stats=]); +// Description: +// ![Metaball animation](https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/images/metaball_demo.gif) +// . +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", +// can produce smoothly varying blobs and organic forms. You create metaballs by placing metaball +// objects at different locations. These objects have a basic size and shape when placed in +// isolation, but if another metaball object is nearby, the two objects interact, growing larger +// and melding together. The closer the objects are, the more they blend and meld. +// . +// The simplest metaball specification is a 1D list of alternating transformation matrices and +// metaball functions: `[trans0, func0, trans1, func1, ... ]`. Each transformation matrix +// you supply can be constructed using the usual transformation commands such as {{up()}}, +// {{right()}}, {{back()}}, {{move()}}, {{scale()}}, {{rot()}} and so on. You can multiply +// the transformations together, similar to how the transformations can be applied +// to regular objects in OpenSCAD. For example, to transform an object in regular OpenSCAD you +// might write `up(5) xrot(25) zrot(45) scale(4)`. You would provide that transformation +// as the transformation matrix `up(5) * xrot(25) * zrot(45) * scale(4)`. You can use +// scaling to produce an ellipsoid from a sphere, and you can even use {{skew()}} if desired. +// When no transformation is needed, give `IDENT` as the transformation. +// . +// The metaballs are evaluated over a bounding box, which can be a scalar-size cube or specified by its +// minimum and maximum corners `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. The contributions from **all** +// metaballs, even those outside the box, are evaluated over the bounding box. This bounding box is +// divided into voxels of the specified `voxel_size`, which can also be a scalar cube or a vector size. +// Smaller voxels produce a finer, smoother result at the expense of execution time. By default, if the +// voxel size doesn't exactly divide your specified bounding box, then the bounding box is enlarged to +// contain whole voxels, and centered on your requested box. Alternatively, you may set +// `exact_bounds=true` to cause the voxels to adjust in size to fit instead. Either way, if the +// bounding box clips a metaball and `closed=true` (the default), the object is closed at the +// intersection surface. Setting `closed=false` causes the [VNF](vnf.scad) faces to end at the bounding +// box, resulting in a non-manifold shape with holes, exposing the inside of the object. +// . +// For metaballs with flat surfaces (the ends of `mb_cyl()`, and `mb_cuboid()` with `squareness=1`), +// avoid letting any side of the bounding box coincide with one of these flat surfaces, otherwise +// unpredictable triangulation around the edge may result. +// . +// You can create metaballs in a variety of standard shapes using the predefined functions +// listed below. If you wish, you can also create custom metaball shapes using your own functions +// (see Examples 20 and 21). For all of the built-in metaballs, three parameters are available to control +// the interaction of the metaballs with each other: `cutoff`, `influence`, and `negative`. +// . +// The `cutoff` parameter specifies the distance beyond which the metaball has no interaction +// with other balls. When you apply `cutoff`, a smooth suppression factor begins +// decreasing the interaction strength at half the cutoff distance and reduces the interaction to +// zero at the cutoff. Note that the smooth decrease may cause the interaction to become negligible +// closer than the actual cutoff distance, depending on the voxel size and `influence` of the +// ball. Also, depending on the value of `influence`, a cutoff that ends in the middle of +// another ball can result in strange shapes, as shown in Example 17, with the metaball +// interacting on one side of the boundary and not interacting on the other side. If you scale +// a ball, the cutoff value is also scaled. The exact way that cutoff is defined +// geometrically varies for different ball types; see below for details. +// . +// The `influence` parameter adjusts the strength of the interaction that metaball objects have with +// each other. If you increase `influence` of one metaball from its default of 1, then that metaball +// interacts with others at a longer range, and surrounding balls grow bigger. The metaball with larger +// influence can also grow bigger because it couples more strongly with other nearby balls, but it +// can also remain nearly unchanged while influencing others when `isovalue` is greater than 1. +// Decreasing influence has the reverse effect. Small changes in influence can have a large +// effect; for example, setting `influence=2` dramatically increases the interactions at longer +// distances, and you may want to set the `cutoff` argument to limit the range influence. +// At the other exteme, small influence values can produce ridge-like artifacts or texture on the +// model. Example 14 demonstrates this effect. To avoid these artifacts, keep `influence` above about +// 0.5 and consider using `cutoff` instead of using small influence. +// . +// The `negative` parameter, if set to `true`, creates a negative metaball, which can result in +// hollows, dents, or reductions in size of other metaballs. +// Negative metaballs are never directly visible; only their effects are visible. The `influence` +// argument may also behave in ways you don't expect with a negative metaball. See Examples 16 and 17. +// . +// For complicated metaball assemblies you may wish to repeat a structure in different locations or +// otherwise transformed. Nested metaball specifications are supported: +// Instead of specifying a transform and function, you specify a transform and then another metaball +// specification. For example, you could set `finger=[t0,f0,t1,f1,t2,f2]` and then set +// `hand=[u0,finger,u1,finger,...]` and then invoke `metaballs()` with `[s0, hand]`. +// In effect, any metaball specification array can be treated as a single metaball in another specification array. +// This is a powerful technique that lets you make groups of metaballs that you can use as individual +// metaballs in other groups, and can make your code compact and simpler to understand. See Example 23. +// . +// The isovalue parameter applies globally to **all** your metaballs and changes the appearance of your +// entire metaball object, possibly dramatically. It defaults to 1 and you don't usually need to change +// it. If you increase the isovalue, then all the objects in your model shrink, causing some melded +// objects to separate. If you decrease it, each metaball grows and melds more with others. +// . +// ***Built-in metaball functions*** +// . +// Several metaballs are defined for you to use in your models. +// All of the built-in metaballs take positional and named parameters that specify the size of the +// metaball (such as height or radius). The size arguments are the same as those for the regular objects +// of the same type (e.g. a sphere accepts both `r` for radius and the named parameter `d=` for +// diameter). The size parameters always specify the size of the metaball **in isolation** with +// `isovalue=1`. The metaballs can grow much bigger than their specified sizes when they interact +// with each other. Changing `isovalue` also changes the sizes of metaballs. They grow bigger than their +// specified sizes, even in isolation, if `isovalue < 1` and smaller than their specified sizes if +// `isovalue > 1`. +// . +// The built-in metaball functions are listed below. As usual, arguments without a trailing `=` can be used positionally; arguments with a trailing `=` must be used as named arguments. +// The examples below illustrates each type of metaball interacting with another of the same type. +// . +// * `mb_sphere(r|d=)` — spherical metaball, with radius r or diameter d. You can create an ellipsoid using `scale()` as the last transformation entry of the metaball `spec` array. +// * `mb_cuboid(size, [squareness=])` — cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` specifies the width of the cuboid shape between the face centers; `size` may be a scalar or a vector, as in {{cuboid()}}. Except when `squareness=1`, the faces are always a little bit curved. +// * `mb_cyl(h|l|height|length, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=])` — vertical cylinder or cone metaball with the same dimenional arguments as {{cyl()}}. At least one of the radius or diameter arguments is required. The `rounding` argument defaults to 0 (sharp edge) if not specified. Only one rounding value is allowed: the rounding is the same at both ends. For a fully rounded cylindrical shape, consider using `mb_capsule()` or `mb_disk()`, which are less flexible but have faster execution times. +// * `mb_disk(h|l|height|length, r|d=)` — rounded disk with flat ends. The diameter specifies the total diameter of the shape including the rounded sides, and must be greater than its height. +// * `mb_capsule(h|l|height|length, [r|d=], [r1=|d1=], [r2=|d2=])` — vertical cylinder or cone with rounded caps, using the same dimensional arguments as {{cyl()}}. The object resembles two spheres with a hull around them. The height or length specifies the distance between the spherical centers of the ends. Cutoff is measured from the line segment between the two cap centers. +// * `mb_connector(p1, p2, [r|d=], [r1=|d1=], [r2=|d2=])` — a connecting rod of radius `r` or diameter `d` with hemispherical caps (like `mb_capsule()`), but specified to connect point `p1` to point `p2` (where `p1` and `p2` must be different 3D coordinates). As with `mb_capsule()`, the radius of each cap can be different, and the object resembles two spheres wrapped in a null. The points `p1` and `p2` are at the centers of the two round caps. The connectors themselves are still influenced by other metaballs, but it may be undesirable to have them influence others, or each other. If two connectors are connected, the joint may appear swollen unless `influence` or `cutoff` is reduced. Reducing `cutoff` is preferable if feasible, because reducing `influence` can produce interpolation artifacts. +// * `mb_torus([r_maj|d_maj=], [r_min|d_min=], [or=|od=], [ir=|id=])` — torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. You must provide a combination of inputs that completely specifies the torus. If `cutoff` is applied, it is measured from the circle represented by `r_min=0`. +// * `mb_octahedron(r|d=])` — octahedral metaball with sharp edges and corners. The `r` parameter specifies the distance from center to tip, while `d=` is the distance between two opposite tips. +// . +// In addition to the dimensional arguments described above, all of the built-in functions accept the +// following named arguments: +// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center unless otherwise noted above. Default: INF +// * `influence` — a positive number specifying the strength of interaction this ball has with other balls. Default: 1 +// * `negative` — when true, creates a negative metaball. Default: false +// . +// ***Metaball functions and user defined functions*** +// . +// You can construct complicated metaball models using only the built-in metaball functions above. +// However, you can create your own custom metaballs if desired. +// . +// When multiple metaballs are in a model, their functions are summed and compared to the isovalue to +// determine the final shape of the metaball object. +// Each metaball is defined as a function of a 3-vector that gives the value of the metaball function +// for that point in space. As is common in metaball implementations, we define the built-in metaballs using an +// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance measured from +// the center or core of the metaball. The spherical metaball therefore has a simple basic definition as +// $f(v) = 1/\text{norm}(v)$. If we choose an isovalue $c$, then the set of points $v$ such that $f(v) >= c$ +// defines a bounded set; for example, a sphere with radius depending on the isovalue $c$. The +// default isovalue is $c=1$. Increasing the isovalue shrinks the object, and decreasing the isovalue grows +// the object. +// . +// To adjust interaction strength, the influence parameter applies an exponent, so if `influence=a` +// then the decay becomes $\frac{1}{d^{\frac 1 a}}$. This means, for example, that if you set influence to +// 0.5 you get a $\frac{1}{d^2}$ falloff. Changing this exponent changes how the balls interact. +// . +// You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// that takes a single argument (a 3-vector) and returns a single numerical value. +// Generally, the function should return a scalar value that drops below the isovalue somewhere within your +// bounding box. If you want your custom metaball function to behave similar to to the built-in functions, +// the return value should fall off with distance as $1/d$. See Examples 20, 21, and 22 for demonstrations +// of creating custom metaball functions. Example 22 also shows how to make a metaball that works wtih +// `influence` and `cutoff`. +// . +// ***Voxel size and bounding box*** +// . +// The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. +// A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the +// calculation and storage of 8,000,000 function values, and more processing and memory to generate +// the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box +// requires only 8,000 function values and a modest computation time. A good rule is to keep the number +// of voxels below 10,000 for preview, and adjust the voxel size smaller for final rendering. Omitting both +// `voxel_size` and `voxel_count` arguments automatically sets a voxel size such that approximately 10,000 +// voxels fit within your bounding box, which should be reasonable for initial preview. Because a bounding +// box that is too large wastes time computing function values that are not needed, you can also set the +// parameter `show_stats=true` to get the actual bounds of the voxels intersected by the surface. With this +// information, you may be able to decrease run time, or keep the same run time but increase the resolution. +// . +// ***Duplicated vertices*** +// . +// The point list in the generated VNF structure contains many duplicated points. This is normally not a +// problem for rendering the shape, but machine roundoff differences may result in Manifold issuing +// warnings when doing the final render, causing rendering to abort if you have enabled the "stop on +// first warning" setting. You can prevent this by passing the VNF through {{vnf_quantize()}} using a +// quantization of 1e-7, or you can pass the VNF structure into {{vnf_merge_points()}}, which also +// removes the duplicates. Additionally, flat surfaces (often resulting from clipping by the bounding +// box) 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 computationally expensive +// and are not normally necessary. +// Arguments: +// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification. See above for more details, and see Example 23 for a demonstration. +// bounding_box = A designation of volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]` specifying the minimum and maximum box corner coordinates. By default, the actual bounding box is enlarged if necessary to fit whole voxels, and centered around your requested box. Set `exact_bounds=true` to hold the box size fixed, in which case the voxel changes size instead. +// voxel_size = size of the voxel that is used to sample the bounding box volume. This can be undef, a scalar size for a cubical voxel, or a 3-vector if you want non-cubical voxels. For `undef`, the voxel size is set so that approximately `voxel_count` quantity of voxels fit inside the bounding box. If both `voxel_size=undef` and `voxel_count=undef`, then a fast preview is generated using about 10000 voxels. If you set `exact_bounds=true`, then bounding box is held fixed in size, and the voxel size is adjusted as needed so that whole voxels fit inside the bounding box. +// voxel_count = Approximate quanity of voxels to have in the bounding box when `voxel_size` is not given. If both `voxel_size=undef` and `voxel_count=undef`, then a fast preview is generated using about 10000 voxels. Use with `show_stats=true` to see the corresponding voxel size. +// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0 +// --- +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face, possibly producing non-manfold metaballs with holes where the bounding box intersects them. Default: true +// exact_bounds = When true, shrinks voxels as needed to fit whole voxels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. Default: false +// show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of voxels that the surface passes through, and the number of triangles making up the surface, this is useful for getting information about a possibly smaller bounding box to improve speed for subsequent renders. Enabling this parameter has a small speed penalty. Default: false +// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(3D,NoAxes): Two spheres interacting. +// spec = [ +// left(9), mb_sphere(5), +// right(9), mb_sphere(5) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-16,-7,-7], [16,7,7]]); +// Example(3D,NoAxes): Two rounded cuboids interacting. +// spec = [ +// move([-8,-5,-5]), mb_cuboid(10), +// move([8,5,5]), mb_cuboid(10) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-15,-12,-12], [15,12,12]]); +// Example(3D,NoAxes): Two rounded `mb_cyl()` cones interacting. +// spec = [ +// left(10), mb_cyl(15, r1=6, r2=4, rounding=2), +// right(10), mb_cyl(15, r1=6, r2=4, rounding=2) +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-17,-8,-10], [17,8,10]]); +// Example(3D,NoAxes): Two disks interacting. Here the arguments are in order and not named. +// metaballs([ +// move([-10,0,2]), mb_disk(5,9), +// move([10,0,-2]), mb_disk(5,9) +// ], [[-20,-10,-6], [20,10,6]], 0.5); +// Example(3D,NoAxes): Two capsules interacting. +// metaballs([ +// move([-8,0,4])*yrot(90), mb_capsule(16,3), +// move([8,0,-4])*yrot(90), mb_capsule(16,3) +// ], [[-17,-5,-8], [17,5,8]], 0.5); +// Example(3D,NoAxes): A sphere with two connectors. +// path = [[-20,0,0], [0,0,1], [0,-10,0]]; +// spec = [ +// move(path[0]), mb_sphere(6), +// for(seg=pair(path)) each +// [IDENT, mb_connector(seg[0],seg[1], +// 2, influence=0.5)] +// ]; +// metaballs(spec, voxel_size=0.5, +// bounding_box=[[-27,-13,-7], [4,7,14]]); +// Example(3D,NoAxes): Interaction between two tori in different orientations. +// spec = [ +// move([-10,0,17]), mb_torus(r_maj=6, r_min=2), +// move([7,6,21])*xrot(90), mb_torus(r_maj=7, r_min=3) +// ]; +// voxel_size = 0.5; +// boundingbox = [[-19,-9,9], [18,10,32]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,NoAxes,VPR=[75,0,20]): Two octahedrons interacting. Here `voxel_size` is not given, so it defaults to a value that results in approximately 10,000 voxels in the bounding box. Adding the parameter `show_stats=true` displays the voxel size used, along with other information. +// metaballs([ +// move([-10,0,3]), mb_octahedron(8), +// move([10,0,-3]), mb_octahedron(8) +// ], [[-21,-11,-13], [21,11,13]], 0.5); +// Example(3D,VPD=110): These next five examples demonstrate the different types of metaball interactions. We start with two spheres 30 units apart. Each would have a radius of 10 in isolation, but because they are influencing their surroundings, each sphere mutually contributes to the size of the other. The sum of contributions between the spheres add up so that a surface plotted around the region exceeding the threshold defined by `isovalue=1` looks like a peanut shape surrounding the two spheres. +// spec = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10) +// ]; +// voxel_size = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (5 units from the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. The right sphere is not abruptly cut off because the cutoff function is smooth and influence is normal. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself. +// spec = [ +// left(15), mb_sphere(10, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxel_size = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,VPD=110): Here, the left sphere has less influence in addition to a cutoff. Setting `influence=0.5` results in a steeper falloff of contribution from the left sphere. Each sphere has a different size and shape due to unequal contributions based on distance. +// spec = [ +// left(15), mb_sphere(10, influence=0.5, cutoff=25), +// right(15), mb_sphere(10) +// ]; +// voxel_size = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,VPD=110): In this example, we have two size-10 spheres as before and one tiny sphere of 1.5 units radius offset a bit on the y axis. With an isovalue of 1, this figure would appear similar to Example 9 above, but here the isovalue has been set to 2, causing the surface to shrink around a smaller volume values greater than 2. Remember, higher isovalue thresholds cause metaballs to shrink. +// spec = [ +// left(15), mb_sphere(10), +// right(15), mb_sphere(10), +// fwd(15), mb_sphere(1.5) +// ]; +// voxel_size = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, boundingbox, voxel_size, +// isovalue=2); +// Example(3D,VPD=110): Keeping `isovalue=2`, the influence of the tiny sphere has been set quite high, to 10. Notice that the tiny sphere shrinks a bit, but it has dramatically increased its contribution to its surroundings, causing the two other spheres to grow and meld into each other. The `influence` argument on a small metaball affects its surroundings more than itself. +// spec = [ +// move([-15,0,0]), mb_sphere(10), +// move([15,0,0]), mb_sphere(10), +// move([0,-15,0]), mb_sphere(1.5, influence=10) +// ]; +// voxel_size = 1; +// boundingbox = [[-30,-19,-19], [30,19,19]]; +// metaballs(spec, boundingbox, voxel_size, +// isovalue=2); +// Example(3D,Med): Setting `influence` to less than 0.5 can cause interpolation artifacts in the surface. The only difference between these two spheres is `influence`. Both have `cutoff` set to prevent them from affecting each other. The sphere on the right has a low influence of 0.02, which translates to a falloff with distance $d$ proportional to $\frac{1}{d^50}$. That high exponent increases the *non-linear* nature of the function gradient at the isosurface, reducing the accuracy of the *linear* interpolation of where the the surface intersects each voxel, causing ridges to appear. You could use this to create a texture deliberately, but it is usually better to use `cutoff` to limit the range of influence rather than reducing `influence` significantly below 1. +// spec = [ +// left(10), mb_sphere(8, cutoff=10, influence=1), +// right(10), mb_sphere(8, cutoff=10, influence=0.02) +// ]; +// bbox = [[-18,-8,-8], [18,8,8]]; +// metaballs(spec, bounding_box=bbox, voxel_size=0.4); +// Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. Here instead of setting `voxel_size`, we set `voxel_count` for approximate number of voxels in the bounding box, and the voxel size is adjusted to fit. Setting `exact_bounds=true` forces the bounding box to be fixed, and a non-cubic voxel is then used to fit within that box. +// spec = [ // spheres of different sizes +// move([-20,-20,-20]), mb_sphere(5), +// move([0,-20,-20]), mb_sphere(4), +// IDENT, mb_sphere(3), +// move([0,0,20]), mb_sphere(5), +// move([20,20,10]), mb_sphere(7) +// ]; +// voxel_size = 1.5; +// boundingbox = [[-30,-31,-31], [32,31,30]]; +// metaballs(spec, boundingbox, +// exact_bounds=true, voxel_count=40000); +// Example(3D,NoAxes): A metaball can be negative. 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]]; +// spec = [ +// move(centers[0]), mb_sphere(8), +// move(centers[1]), mb_sphere(3, negative=true) +// ]; +// voxel_size = 0.25; +// boundingbox = [[-7,-6,-6], [3,6,6]]; +// %metaballs(spec, boundingbox, voxel_size); +// color("green") move_copies(centers) sphere(d=1, $fn=16); +// Example(3D,VPD=105,VPT=[3,5,4.7]): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink, but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball has high influence, and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin. +// spec = [ +// back(10), mb_sphere(20), +// IDENT, mb_sphere(2, influence=30, +// cutoff=20, negative=true), +// ]; +// voxel_size = 0.5; +// boundingbox = [[-20,-4,-20], [20,30,20]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges. +// spec = [ +// move([-7,-3,27])*zrot(55), mb_cuboid(6, squareness=1), +// move([5,5,21]), mb_cuboid(5), +// move([10,0,10]), mb_octahedron(5) +// ]; +// voxel_size = 0.5; // a bit slow at this resolution +// boundingbox = [[-12,-9,3], [18,10,32]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,NoAxes,VPD=205,Med): A toy airplane, constructed only from metaball spheres with scaling. The bounding box is used to clip the wingtips, tail, and belly of the fuselage. +// bounding_box = [[-55,-50,-5],[35,50,17]]; +// spec = [ +// move([-20,0,0])*scale([25,4,4]), mb_sphere(1), // fuselage +// move([30,0,5])*scale([4,0.5,8]), mb_sphere(1), // vertical stabilizer +// move([30,0,0])*scale([4,15,0.5]), mb_sphere(1), // horizontal stabilizer +// move([-15,0,0])*scale([6,45,0.5]), mb_sphere(1) // wing +// ]; +// voxel_size = 1; +// color("lightblue") metaballs(spec, bounding_box, voxel_size); +// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Custom metaballs are an advanced technique in which you define your own metaball shape by passing a function literal that takes a single argument: a coordinate in space relative to the metaball center called `point` here, but can be given any name. This distance vector from the origin is calculated internally and always passed to the function. Inside the function, it is converted to a scalar distance `dist`. The function literal expression sets all of your parameters. Only `point` is not set, and it becomes the single parameter to the function literal. The `spec` argument invokes your custom function as a function literal that passes `point` into it. +// function threelobe(point) = +// let( +// ang=atan2(point.y, point.x), +// r=norm([point.x,point.y])*(1.3+cos(3*ang)), +// dist=norm([point.z, r]) +// ) 3/dist; +// metaballs( +// spec = [ +// IDENT, function (point) threelobe(point), +// up(7), mb_sphere(r=4) +// ], +// bounding_box = [[-14,-12,-5],[8,12,13]], +// voxel_size=0.5); +// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Here is a function nearly identical to the previous example, introducing additional dimensional parameters into the function to control its size and number of lobes. The bounding box size here is as small as possible for calculation efficiency, but if you expiriment with this using different argument values, you should increase the bounding box along with voxel size. +// function multilobe(point, size, lobes) = +// let( +// ang=atan2(point.y, point.x), +// r=norm([point.x,point.y])*(1.3+cos(lobes*ang)), +// dist=norm([point.z, r]) +// ) size/dist; +// metaballs( +// spec = [ +// left(7), +// function (point) multilobe(point, 3, 4), +// right(7)*zrot(60), +// function (point) multilobe(point, 3, 3) +// ], +// bounding_box = [[-16,-13,-5],[18,13,6]], +// voxel_size=0.4); +// Example(3D): Next we show how to create a function that works like the built-ins. **This is a full-fledged implementation** that allows you to specify the function directly by name in the `spec` argument without needing the function literal syntax, and without needing the `point` argument in `spec`, as in the prior examples. You must define a calculation function that accepts the `point` position argument and then whatever other parameters your metaball uses (here `r` and `noise_level`). Then there is a "master" function that does some error checking and returns a function literal expression that sets all of your parameters. The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to use this implementation in your own custom functions; in fact it's easier simply to declare the function literal in your `spec` argument, but this example shows how to do it all. +// // +// // noisy sphere internal calculation function +// +// function noisy_sphere_calcs(point, r, noise_level, cutoff, exponent, neg) = +// let( +// noise = rands(0, noise_level, 1)[0], +// dist = norm(point) + noise +// ) neg * mb_cutoff(dist,cutoff) * (r/dist)^exponent; +// +// // noisy sphere "master" entry function to use in spec argument +// +// function noisy_sphere(r, noise_level, cutoff=INF, influence=1, negative=false, d) = +// assert(is_num(cutoff) && cutoff>0, "\ncutoff must be a positive number.") +// assert(is_finite(influence) && influence>0, "\ninfluence must be a positive number.") +// let( +// r = get_radius(r=r,d=d), +// dummy=assert(is_finite(r) && r>0, "\ninvalid radius or diameter."), +// neg = negative ? -1 : 1 +// ) // pass control as a function literal to the calc function +// function (point) noisy_sphere_calcs(point, r, noise_level, cutoff, 1/influence, neg); +// +// // define the scene and render it +// +// spec = [ +// left(9), mb_sphere(5), +// right(9), noisy_sphere(r=5, noise_level=0.2) +// ]; +// voxel_size = 0.5; +// boundingbox = [[-16,-8,-8], [16,8,8]]; +// metaballs(spec, boundingbox, voxel_size); +// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A more complex example using ellipsoids, a capsule, 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 clipping with the bottom of the bounding box. The center of the object is thick due to the contributions of three ellipsoids and a capsule converging. Designing an object like this using metaballs requires trial and error with low-resolution renders. +// include +// tetpts = zrot(15, p = 22 * regular_polyhedron_info("vertices", "tetrahedron")); +// tettransform = [ for(pt = tetpts) move(pt)*rot(from=RIGHT, to=pt)*scale([7,1.5,1.5]) ]; +// +// spec = [ +// // vertical cylinder arm +// up(15), mb_capsule(17, 2, influence=0.8), +// // ellipsoid arms +// for(i=[0:2]) each [tettransform[i], mb_sphere(1, cutoff=30)], +// // ring on top +// up(35)*xrot(90), mb_torus(r_maj=8, r_min=2.5, cutoff=35), +// // feet +// for(i=[0:2]) each [move(2.2*tetpts[i]), mb_sphere(5, cutoff=30)], +// ]; +// voxel_size = 1; +// boundingbox = [[-22,-32,-13], [36,32,46]]; +// // useful to save as VNF for copies and manipulations +// vnf = metaballs(spec, boundingbox, voxel_size, isovalue=1); +// vnf_polyhedron(vnf); +// Example(3D,Med,NoAxes,VPR=[70,0,30],VPD=520,VPT=[0,0,80]): This example demonstrates grouping metaballs together and nesting them in lists of other metaballs, to make a crude model of a hand. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight ellipsoid dent subtracted to hollow out the palm, to make the hand. +// joints = [[0,0,1], [0,0,85], [0,-5,125], [0,-16,157], [0,-30,178]]; +// finger = [ +// for(i=[0:3]) each +// [IDENT, mb_connector(joints[i], joints[i+1], 9+i/5, influence=0.22)] +// ]; +// thumb = [ +// for(i=[0:2]) each [ +// scale([1,1,1.2]), +// mb_connector(joints[i], joints[i+1], 9+i/2, influence=0.28) +// ] +// ]; +// allfingers = [ +// left(15)*zrot(5)*yrot(-50)*scale([1,1,0.6])*zrot(30), thumb, +// left(15)*yrot(-9)*scale([1,1,0.9]), finger, +// IDENT, finger, +// right(15)*yrot(8)*scale([1,1,0.92]), finger, +// right(30)*yrot(17)*scale([0.9,0.9,0.75]), finger +// ]; +// hand = [ +// IDENT, allfingers, +// move([-5,0,5])*scale([1,0.36,1.55]), mb_cuboid(90, squareness=0.3, cutoff=80), +// move([-10,-95,50])*yrot(10)*scale([2,2,0.95]), +// mb_sphere(r=15, cutoff=50, influence=1.5, negative=true) +// ]; +// voxel_size=2.5; +// bbox = [[-104,-40,-10], [79,18,188]]; +// metaballs(hand, bbox, voxel_size, isovalue=1); +// Example(3D,Med,NoAxes,VPR=[76,0,40],VPD=128,VPT=[4,-1,13]): A model of an elephant using cylinders, capsules, and disks. +// legD1 = 4.6; +// legD2 = 1; +// spec = [ +// // legs +// up(1)*fwd(8)*left(13), mb_cyl(d1=legD1, d2=legD2, h=20), +// up(1)*fwd(8)*right(10), mb_cyl(d1=legD1, d2=legD2, h=20), +// up(1)*back(8)*left(13), mb_cyl(d1=legD1, d2=legD2, h=20), +// up(1)*back(8)*right(10), mb_cyl(d1=legD1, d2=legD2, h=20), +// up(20)*yrot(90), mb_capsule(d=21, h=36, influence=0.5), // body +// right(21)*up(25)*yrot(-20), mb_capsule(r=7, h=25, influence=0.5, cutoff=9), // head +// right(24)*up(10)*yrot(15), mb_cyl(d1=3, d2=6, h=15, cutoff=3), // trunk +// // ears +// right(18)*up(29)*fwd(11)*zrot(-20)*yrot(80)*scale([1.4,1,1]), mb_disk(r=5,h=2, cutoff=3), +// right(18)*up(29)*back(11)*zrot(20)*yrot(80)*scale([1.4,1,1]), mb_disk(r=5,h=2, cutoff=3), +// // tusks +// right(26)*up(13)*fwd(5)*yrot(135), mb_capsule(r=1, h=10, cutoff=1), +// right(26)*up(13)*back(5)*yrot(135), mb_capsule(r=1, h=10, cutoff=1) +// ]; +// bbox = [[-21,-17,-9], [31,17,38]]; +// metaballs(spec, bounding_box=bbox, voxel_size=1, isovalue=1); +// Example(3D,NoAxes,Med,VPD=235,VPR=[83,0,320],VPT=[-5,-5,43]): A model of a giraffe using a variety of different metaball shapes. Features such as the tail and lower legs are thin, so a small voxel size is required to render them. +// legD = 1; +// tibia = 14; +// femur = 12; +// head = [-35,0,78]; // head position +// stance = [12,6]; // leg position offsets +// +// spec = [ +// // Legs +// move([-stance.x,-stance.y]), mb_connector([-4,0,0],[-6,0,tibia],legD, influence = 0.2), +// move([-stance.x,stance.y]), mb_connector([0,0,0],[0,0,tibia],legD, influence = 0.2), +// move([stance.x,-stance.y]), mb_connector([-2,0,0],[-3,0,tibia],legD, influence = 0.2), +// move([stance.x,stance.y]), mb_connector([0,0,0],[0,0,tibia],legD, influence = 0.2), +// +// move([-stance.x,-stance.y,tibia]), mb_connector([-6,0,0],[-2,0,femur],legD), +// move([-stance.x,stance.y,tibia]), mb_connector([0,0,0],[0,0,femur],legD), +// move([stance.x,-stance.y,tibia]), mb_connector([-3,0,0],[-1,0,femur],legD), +// move([stance.x,stance.y,tibia]), mb_connector([0,0,0],[0,0,femur],legD), +// +// // Hooves +// move([-stance.x-6,-stance.y,1]), mb_capsule(d= 2, h = 3, cutoff = 2), +// move([-stance.x-1,stance.y,1]), mb_capsule(d= 2, h = 3, cutoff = 2), +// move([stance.x-3.5,-stance.y,1]), mb_capsule(d= 2, h = 3, cutoff = 2), +// move([stance.x-1,stance.y,1]), mb_capsule(d= 2, h = 3, cutoff = 2), +// +// // Body +// up(tibia+femur+10) * yrot(10), mb_cuboid([16,7,7]), +// up(tibia+femur+15)*left(10), mb_sphere(2), +// up(tibia+femur+8)*right(13)*xrot(90), mb_disk(1,4), +// +// // Tail +// up(tibia+femur+8), mb_connector([18,0,0],[22,0,-16], 0.4, cutoff = 1), +// +// // Neck +// up(tibia+femur+35)*left(22)*yrot(-30)* yscale(0.75), mb_cyl(d1 = 5, d2 = 3, l = 38), +// +// // Head +// move(head + [-4,0,-3])*yrot(45)*xscale(0.75), mb_cyl(d1 = 1.5, d2 = 4, l = 12, rounding=0), +// move(head), mb_cuboid(2), +// +// // Horns +// move(head), mb_connector([0,-2,5],[0,-2.5,8],0.3, cutoff = 1), +// move(head + [0,-2.5,8]), mb_sphere(0.5, cutoff = 1), +// move(head), mb_connector([0,2,5],[0,2.5,8],0.3, cutoff = 1), +// move(head + [0,2.5,8]), mb_sphere(0.5, cutoff = 1), +// +// // Ears +// move(head + [2,-8,4])* xrot(60) * scale([0.5,1,3]) , mb_sphere(d = 2, cutoff = 2), +// move(head + [2,8,4])* xrot(-60) * scale([0.5,1,3]) , mb_sphere(d = 2, cutoff = 2), +// ]; +// vsize = 0.85; +// bbox = [[-45.5, -11.5, 0], [23, 11.5, 87.55]]; +// metaballs(spec, bbox, voxel_size=vsize); +// Example(3D,Med,NoAxes): A model of a bunny, made from separate body components made with metaballs, with each component rendered at a different voxel size, and then combined together along with eyes and teeth. In this way, smaller bounding boxes can be defined for each component, which speeds up rendering. A bit more time is saved by saving the repeated components (ear, front leg, hind leg) in VNF structures, to render copies with {{vnf_polyhedron()}}. +// torso = [ +// up(20) * scale([1,1.2,2]), mb_sphere(10), +// up(10), mb_sphere(5) // fatten lower torso +// ]; +// head = [ +// up(50) * scale([1.2,0.8,1]), mb_sphere(10, cutoff = 15), +// // nose +// move([0,-11,50]), mb_cuboid(2), +// // eye sockets +// move([5,-10,54]), mb_sphere(0.5, negative = true), +// move([-5,-10,54]), mb_sphere(0.5, negative = true), +// // tail +// move([0,15,6]), mb_sphere(2, cutoff = 5) +// ]; +// hind_leg = [ +// move([-15,-5,3]) * scale([1.5,4,1.75]), mb_sphere(5), +// move([-15,10,3]), mb_sphere(3, negative = true) +// ]; +// front_leg = [ +// move([-9,-4,30]) * zrot(30) * scale([1.5,5,1.75]), mb_sphere(3), +// move([-9,10,30]), mb_sphere(2, negative = true) +// ]; +// ear = [ +// yrot(10) * move([0,0,65]) * scale([4,1,7]), mb_sphere(2), +// yrot(10)*move([0,-3,65])*scale([3,2,6]), mb_sphere(2, cutoff = 2, influence =2, negative = true) +// ]; +// vnf_hindleg = metaballs(hind_leg, [[-22,-24,0],[-8,7,11]], voxel_size=0.8); +// vnf_frontleg = metaballs(front_leg, [[-16,-17,25], [-1,7,35]], voxel_size=0.6); +// vnf_ear = metaballs(ear, [[3,-2,50],[20,2,78]], voxel_size=0.6); +// color("BurlyWood") { +// metaballs([IDENT, torso, IDENT, head], +// [[-16,-17,0],[16,20,63]], voxel_size=0.7); +// xflip_copy() { +// vnf_polyhedron(vnf_hindleg); +// vnf_polyhedron(vnf_frontleg); +// vnf_polyhedron(vnf_ear);; +// } +// } +// // add eyes +// xflip_copy() move([5,-8,54]) color("skyblue") sphere(2, $fn = 32); +// // add teeth +// xflip_copy() move([1.1,-10,44]) color("white") cuboid([2,0.5,4], rounding = 0.15); + +module metaballs(spec, bounding_box, voxel_size, voxel_count, isovalue=1, closed=true, exact_bounds=false, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, show_box=false) { + vnf = metaballs(spec, bounding_box, voxel_size, voxel_count, isovalue, closed, exact_bounds, show_stats); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); + if(show_box) + let(bbox = _getbbox(voxel_size, bounding_box, exact_bounds, undef)) + %translate(bbox[0]) cube(bbox[1]-bbox[0]); +} + +function metaballs(spec, bounding_box, voxel_size, voxel_count, isovalue=1, closed=true, exact_bounds=false, show_stats=false) = + assert(all_defined([spec, bounding_box]), "\nThe parameters spec and bounding_box must both be defined.") + assert(num_defined([voxel_size, voxel_count])<=1, "\nOnly one of voxel_size or voxel_count can be defined.") + assert(is_undef(voxel_size) || (is_finite(voxel_size) && voxel_size>0) || (is_vector(voxel_size) && all_positive(voxel_size)), "\nvoxel_size must be a positive number, a 3-vector of positive values, or undef.") + assert(is_finite(isovalue) || (is_list(isovalue) && len(isovalue)==2 && is_num(isovalue[0]) && is_num(isovalue[1])), "\nIsovalue must be a number or a range; a number is the same as [number,INF].") + assert(len(spec)%2==0, "\nThe spec parameter must be an even-length list of alternating transforms and functions") + let( + isoval = is_list(isovalue) ? isovalue : [isovalue, INF], + funclist = _mb_unwind_list(spec), + nballs = len(funclist)/2, + dummycheck = [ + for(i=[0:len(spec)/2-1]) let(j=2*i) + assert(is_matrix(spec[j],4,4), str("\nspec entry at position ", j, " must be a 4×4 matrix.")) + assert(is_function(spec[j+1]) || is_list(spec[j+1]), str("\nspec entry at position ", j+1, " must be a function literal or a metaball list.")) 0 + ], + // set up transformation matrices in advance + transmatrix = [ + for(i=[0:nballs-1]) + let(j=2*i) + transpose(select(matrix_inverse(funclist[j]), 0,2)) + ], + + // new voxel or bounding box centered around original, to fit whole voxels + bbox0 = is_num(bounding_box) + ? let(hb=0.5*bounding_box) [[-hb,-hb,-hb],[hb,hb,hb]] + : bounding_box, + autovoxsize = is_def(voxel_size) ? voxel_size : _getautovoxsize(bbox0, default(voxel_count,22^3)), + voxsize = _getvoxsize(autovoxsize, bbox0, exact_bounds), + newbbox = _getbbox(voxsize, bbox0, exact_bounds), + + // set up field array + bot = newbbox[0], + top = newbbox[1], + halfvox = 0.5*voxsize, + // accumulate metaball contributions using matrices rather than sums + xset = [bot.x:voxsize.x:top.x+halfvox.x], + yset = list([bot.y:voxsize.y:top.y+halfvox.y]), + zset = list([bot.z:voxsize.z:top.z+halfvox.z]), + allpts = [for(x=xset, y=yset, z=zset) [x,y,z,1]], + trans_pts = [for(i=[0:nballs-1]) allpts*transmatrix[i]], + allvals = [for(i=[0:nballs-1]) [for(pt=trans_pts[i]) funclist[2*i+1](pt)]], + //total = _sum(allvals,allvals[0]*EPSILON), + total = _sum(slice(allvals,1,-1), allvals[0]), + fieldarray = list_to_matrix(list_to_matrix(total,len(zset)),len(yset)) + ) isosurface(fieldarray, isoval, newbbox, voxsize, closed=closed, exact_bounds=true, show_stats=show_stats, _mball=true); + +/// internal function: unwrap nested metaball specs in to a single list +function _mb_unwind_list(list, parent_trans=[IDENT]) = + let( + dum1 = assert(is_list(list), "\nDid not find valid list of metaballs."), + n=len(list), + dum2 = assert(n%2==0, "\nList of metaballs must have an even number of elements with alternating transforms and functions/lists.") + ) [ + for(i=[0:2:n-1]) + let( + dum = assert(is_matrix(list[i],4,4), str("\nInvalid 4×4 transformation matrix found at position ",i,".")), + trans = parent_trans[0] * list[i], + j=i+1 + ) if(is_function(list[j])) + each [trans, list[j]] + else if (is_list(list[j])) + each _mb_unwind_list(list[j], [trans]) + else + assert(false, str("\nExpected function literal or list at position ",j,".")) + ]; + + + +/// ---------- isosurface stuff starts here ---------- + +// Function&Module: isosurface() +// Synopsis: Creates a 3D isosurface (a 3D contour) from a function or array of values. +// SynTags: Geom,VNF +// Topics: Isosurfaces, VNF Generators +// Usage: As a module +// isosurface(f, isovalue, bounding_box, voxel_size, [voxel_count=], [reverse=], [closed=], [exact_bounds=], [show_stats=], ...) [ATTACHMENTS]; +// Usage: As a function +// vnf = isosurface(f, isovalue, bounding_box, voxel_size, [voxel_count=], [reverse=], [closed=], [exact_bounds=], [show_stats=]); +// Description: +// Computes a [VNF structure](vnf.scad) of an object bounded by an isosurface or a range between two isosurfaces, within a specified bounding box. +// The isosurface of a function $f(x,y,z)$ is the set of points where $f(x,y,z)=c$ for some +// constant isovalue $c$. +// To provide a function, you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) +// taking an `[x,y,z]` coordinate as input to define the grid coordinate location and +// returning a single numerical value. +// You can also define an isosurface using a 3D array of values instead of a function, in which +// case the isosurface is the set of points equal to the isovalue as interpolated from the array. +// The array indices are in the order `[x][y][z]`. +// . +// The isovalue must be specified as a range `[c_min,c_max]`. The range can be finite or unbounded at one +// end, with either `c_min=-INF` or `c_max=INF`. The returned object is the set of points `p` that +// satisfy `c_min <= f(p) <= c_max`. If `f(p)` has values larger than `c_min` and values smaller than +// `c_max`, then the result is a shell object with two bounding surfaces corresponding to the +// isosurfaces at `c_min` and `c_max`. If `f(p) < c_max` +// everywhere (which is true when `c_max = INF`), then no isosurface exists for `c_max`, so the object +// has only one bounding surface: the one defined by `c_min`. This can result in a bounded object +// like a sphere, or it can result an an unbounded object such as all the points outside of a sphere out +// to infinity. A similar situation arises if `f(p) > c_min` everywhere (which is true when +// `c_min = -INF`). Setting isovalue to `[-INF,c_max]` or `[c_min,INF]` always produces an object with a +// single bounding isosurface, which itself can be unbounded. To obtain a bounded object, think about +// whether the function values inside your object are smaller or larger than your isosurface value. If +// the values inside are smaller, you produce a bounded object using `[-INF,c_max]`. If the values +// inside are larger, you get a bounded object using `[c_min,INF]`. +// . +// The isosurface is evaluated over a bounding box, which can be a scalar cube, or specified by its +// minimum and maximum corners `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. This bounding box is divided into +// voxels of the specified `voxel_size`, which can also be a scalar cube, or a vector size. Smaller +// voxels produce a finer, smoother result at the expense of execution time. By default, if the voxel +// size doesn't exactly divide your specified bounding box, then the bounding box is enlarged to +// contain whole voxels, and centered on your requested box. Alternatively, you may set +// `exact_bounds=true` to force the voxels to adjust in size to fit instead. +// Either way, if the bounding box clips the isosurface and `closed=true` (the default), a surface is +// added to create a closed manifold object. Setting `closed=false` causes the VNF faces to end at the +// bounding box, resulting in a non-manifold shape that exposes the inside of the object. +// . +// ***Why does my object appear as a cube?*** If your object is unbounded, then when it intersects with +// the bounding box and `closed=true`, the result may appear to be a solid cube, because the clipping +// faces are all you can see and the bounding surface is hidden inside. Setting `closed=false` removes +// the bounding box faces and exposes the inside structure (with inverted faces). If you want the bounded +// object, you can correct this problem by changing your isovalue range. If you were using a finite range +// `[c1,c2]`, try changing it to `[c2,INF]` or `[-INF,c1]`. If you were using an unbounded range like +// `[c,INF]`, try switching the range to `[-INF,c]`. +// . +// ***Run time:*** The `voxel_size` and `bounding_box` parameters affect the run time, which can be long. +// A voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it requires the +// calculation and storage of 8,000,000 function values, and more processing and memory to generate +// the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box +// requires only 8,000 function values and a modest computation time. A good rule is to keep the number +// of voxels below 10,000 for preview, and adjust the voxel size smaller for final rendering. Omitting both +// `voxel_size` and `voxel_count` arguments automatically sets a voxel size such that approximately 10,000 +// voxels fit within your bounding box, which should be reasonable for initial preview. Because a bounding +// box that is too large wastes time computing function values that are not needed, you can also set the +// parameter `show_stats=true` to get the actual bounds of the voxels intersected by the surface. With this +// information, you may be able to decrease run time, or keep the same run time but increase the resolution. +// . +// ***Manifold warnings:*** The point list in the generated VNF structure contains many duplicated points. This is normally not a +// problem for rendering the shape, but machine roundoff differences may result in Manifold issuing +// warnings when doing the final render, causing rendering to abort if you have enabled the "stop on +// first warning" setting. You can prevent this by passing the VNF through {{vnf_quantize()}} using a +// quantization of 1e-7, or you can pass the VNF structure into {{vnf_merge_points()}}, which also +// removes the duplicates. Additionally, flat surfaces (often resulting from clipping by the bounding +// box) 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 computationally expensive +// and are not normally necessary. +// Arguments: +// f = The isosurface function literal or array. As a function literal, `x,y,z` must be the first arguments. +// isovalue = A 2-vector giving an isovalue range. For an unbounded range, use `[-INF, max_isovalue]` or `[min_isovalue, INF]`. +// bounding_box = A designation of volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]` specifying the minimum and maximum box corner coordinates. By default, the actual bounding box is enlarged if necessary to fit whole voxels, and centered around your requested box. Set `exact_bounds=true` to hold the box size fixed, in which case the voxel changes size instead. When `f` is an array of values, `bounding_box` cannot be supplied if `voxel_size` is supplied because the bounding box is already implied by the array size combined with `voxel_size`, in which case this implied bounding box is centered around the origin. +// voxel_size = size of the voxel that is used to sample the bounding box volume. This can be undef, a scalar size for a cubical voxel, or a 3-vector if you want non-cubical voxels. For `undef`, the voxel size is set so that approximately `voxel_count` quantity of voxels fit inside the bounding box. If both `voxel_size=undef` and `voxel_count=undef`, then a fast preview is generated using about 10000 voxels. If you set `exact_bounds=true`, then bounding box is held fixed in size, and the voxel size is adjusted as needed so that whole voxels fit inside the bounding box. +// --- +// voxel_count = Approximate quanity of voxels to have in the bounding box when `voxel_size` is not given. If both `voxel_size=undef` and `voxel_count=undef`, then a fast preview is generated using about 10000 voxels. Use with `show_stats=true` to see the corresponding voxel size. Default: undef +// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true +// reverse = When true, reverses the orientation of the VNF faces. Default: false +// exact_bounds = When true, shrinks voxels as needed to fit whole voxels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. Default: false +// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that the surface passes through, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false +// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false +// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6 +// cp = (Module only) Center point for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` +// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = (Module only) Select "hull" or "intersect" anchor type. Default: "hull" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): These first three examples demonstrate the effect of isovalue range for the simplest of all surfaces: a sphere where $r=\sqrt{x^2+y^2+z^2}$, or `r = norm([x,y,z])` in OpenSCAD. Then, the isosurface corresponding to an isovalue of 10 is every point where the expression `norm([x,y,z])` equals a radius of 10. We use the isovalue range `[-INF,10]` here to make the sphere, with a bounding box that cuts off half the sphere. The isovalue range could also be `[0,10]` because the minimum value of the expression is zero. +// isovalue = [-INF,10]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (x,y,z) norm([x,y,z]), +// isovalue, bbox, voxel_size = 1); +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): An isovalue range `[8,10]` gives a shell with inner radius 8 and outer radius 10. +// isovalue = [8,10]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (x,y,z) norm([x,y,z]), +// isovalue, bbox, voxel_size = 1); +// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): Here we set the isovalue range to `[10,INF]`. Because the sphere expression `norm(xyz)` has larger values growing to infinity with distance from the origin, the resulting object appears as the bounding box with a radius-10 spherical hole. +// isovalue = [10,INF]; +// bbox = [[-11,-11,-11], [0,11,11]]; +// isosurface(function (x,y,z) norm([x,y,z]), +// isovalue, bbox, voxel_size = 1); +// Example(3D,ThrownTogether,NoAxes): Unlike a sphere, a gyroid is unbounded; it's 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 is a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates using an additional parameter in the field function beyond just the `[x,y,z]` input; in this case to control the wavelength of the gyroid. +// function gyroid(x,y,z, wavelength) = let( +// p = 360/wavelength * [x,y,z] +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [0,INF]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function(x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, 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 * [x,y,z] +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [0,INF]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function(x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, bbox, voxel_size=5, closed=true); +// 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 * [x,y,z] +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function(x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, 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. +// function gyroid(x,y,z, wavelength) = let( +// p = 360/wavelength * [x,y,z] +// ) sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x); +// isovalue = [-0.3, 0.3]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function(x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue, 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). +// function schwartz_p(x,y,z, wavelength) = let( +// p = 360/wavelength, +// px = p*x, py = p*y, pz = p*z +// ) cos(px) + cos(py) + cos(pz); +// isovalue = [-0.2, 0.2]; +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(function (x,y,z) schwartz_p(x,y,z, 100), +// isovalue, 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). +// function neovius(x,y,z, wavelength) = let( +// p = 360/wavelength, +// 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(function (x,y,z) neovius(x,y,z, 200), +// isovalue = [-0.3, 0.3], +// bounding_box = bbox, voxel_size=4); +// Example(3D,NoAxes): Example of a bounded isosurface. +// isosurface( +// function (x,y,z) +// let(a=xyz_to_spherical([x,y,z]), +// r=a[0], +// phi=a[1], +// theta=a[2] +// ) 1/(r*(3+cos(5*phi)+cos(4*theta))), +// isovalue = [0.1,INF], +// bounding_box = [[-8,-7,-8],[6,7,8]], +// voxel_size = 0.25); +// Example(3D,NoAxes): Another example of a bounded isosurface. +// isosurface(function (x,y,z) +// 2*(x^4 - 2*x*x + y^4 +// - 2*y*y + z^4 - 2*z*z) + 3, +// bounding_box=3, voxel_size=0.07, +// isovalue=[-INF,0]); +// Example(3D,NoAxes): For shapes that occupy a cubical bounding box centered on the origin, you can simply specify a scalar for the size of the box. +// isosurface( +// function (x,y,z) let(np=norm([x,y,z])) +// (x*y*z^3 + 19*x^2*z^2) / np^2 + np^2, +// isovalue=[-INF,35], bounding_box=12, voxel_size=0.25); +// Example(3D,Med,NoAxes,VPD=165,VPR=[72,0,290],VPT=[0,0,0]): An object that could be a sort of support pillar. Here we set `show_box=true` to reveal that the bounding box is slightly bigger than it needs to be. The argument `show_stats=true` also outputs the voxel bounding box size as a suggestion of what it should be. +// isosurface( +// function (x,y,z) let(np=norm([x,y,z])) +// (x*y*z^3 - 3*x^2*z^2) / np^2 + np^2, +// isovalue=[-INF,35], bounding_box=[[-32,-32,-14],[32,32,14]], +// voxel_size = 0.8, show_box=true); +// Example(3D,NoAxes): You can specify non-cubical voxels for efficiency. This example shows the result of two identical surface functions. The figure on the left uses a `voxel_size=1`, which washes out the detail in the z direction. The figure on the right shows the same shape with `voxel_size=[0.5,1,0.2]` to give a bit more resolution in the x direction and much more resolution in the z direction. This example runs about six times faster than if we used a cubical voxel of size 0.2 to capture the detail in only one axis at the expense of unnecessary detail in other axes. +// function shape(x,y,z, r=5) = +// r / sqrt(x^2 + 0.5*(y^2 + z^2) +// + 0.5*r*cos(200*z)); +// bbox = [[-6,-8,0], [6,8,7]]; +// +// left(6) isosurface(function (x,y,z) shape(x,y,z), +// isovalue=[1,INF], bounding_box=bbox, +// voxel_size=1); +// +// right(6) isosurface(function (x,y,z) shape(x,y,z), +// isovalue=[1,INF], bounding_box=bbox, +// voxel_size=[0.5,1,0.2]); +// Example(3D,NoAxes): Nonlinear functions with steep gradients between voxel corners at the isosurface value can show interpolation ridges because the surface position is approximated by a linear interpolation of a highly nonlinear function. The appearance of the artifacts depends on the combination of function, voxel size, and isovalue, and can look different in different circumstances. If your isovalue is positive, then you may be able to smooth out the artifacts by using the log of your function and the log of your isovalue range to get the same isosurface without artifacts. On the left, an isosurface around a steep nonlinear function (clipped on the left by the bounding box) exhibits severe interpolation artifacts. On the right, the log of the isosurface around the log of the function smooths it out nicely. +// bbox = [[0,-10,-5],[9,10,6]]; +// +// function shape(x,y,z) = +// exp(-((x+5)/5-3)^2-y^2) +// *exp(-((x+5)/3)^2-y^2-z^2) +// + exp(-((y+4)/5-3)^2-x^2) +// *exp(-((y+4)/3)^2-x^2-0.5*z^2); +// +// left(6) isosurface(function(x,y,z) shape(x,y,z), +// isovalue = [EPSILON,INF], +// bounding_box=bbox, voxel_size=0.25); +// right(6) isosurface(function(x,y,z) log(shape(x,y,z)), +// isovalue = [log(EPSILON),INF], +// bounding_box=bbox, voxel_size=0.25); +// Example(3D): Using an array for the `f` argument instead of a function literal. Each row of the array represents an X index for a YZ plane with the array Z indices changing fastest in each plane. The final object may need rotation to get the orientation you want. You don't pass the `bounding_box` argument here; it is implied by the array size and voxel size, and centered on the origin. +// field = [ +// repeat(0,[6,6]), +// [ [0,1,2,2,1,0], +// [1,2,3,3,2,1], +// [2,3,4,4,3,2], +// [2,3,4,4,3,2], +// [1,2,3,3,2,1], +// [0,1,2,2,1,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,1,1,0,0], +// [0,2,3,3,2,0], +// [0,2,3,3,2,0], +// [0,0,1,1,0,0], +// [0,0,0,0,0,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,0,0,0,0], +// [0,1,2,2,1,0], +// [0,1,2,2,1,0], +// [0,0,0,0,0,0], +// [0,0,0,0,0,0] +// ], +// repeat(0,[6,6]) +// ]; +// rotate([0,-90,180]) +// isosurface(field, isovalue=[0.5,INF], +// voxel_size=10); + +module isosurface(f, isovalue, bounding_box, voxel_size, voxel_count=undef, reverse=false, closed=true, exact_bounds=false, convexity=6, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull", show_stats=false, show_box=false, _mball=false) { + vnf = isosurface(f, isovalue, bounding_box, voxel_size, voxel_count, reverse, closed, exact_bounds, show_stats, _mball); + vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype) + children(); + if(show_box) + let(bbox = _getbbox(voxel_size, bounding_box, exact_bounds, f)) + %translate(bbox[0]) cube(bbox[1]-bbox[0]); +} + +function isosurface(f, isovalue, bounding_box, voxel_size, voxel_count=undef, reverse=false, closed=true, exact_bounds=false, show_stats=false, _mball=false) = + + assert(all_defined([f, isovalue]), "\nThe parameters f and isovalue must both be defined.") + assert(num_defined([voxel_size, voxel_count])<=1, "\nOnly one of voxel_size or voxel_count can be defined.") + assert(is_undef(voxel_size) || (is_finite(voxel_size) && voxel_size>0) || (is_vector(voxel_size) && all_positive(voxel_size)), "\nvoxel_size must be a positive number, a 3-vector of positive values, or undef.") + assert(is_list(isovalue) && len(isovalue)==2 && is_num(isovalue[0]) && is_num(isovalue[1]), "\nIsovalue must be a range; use [minvalue,INF] or [-INF,maxvalue] for an unbounded range.") + assert(is_function(f) || + (is_list(f) && + // _mball=true allows voxel_size and bounding_box to coexist with f as array, because metaballs() already calculated them + (_mball || + ((is_def(bounding_box) && is_undef(voxel_size)) || (is_undef(bounding_box) && is_def(voxel_size))) + ) + ) + , "\nWhen f is an array, either bounding_box or voxel_size is required (but not both).") + let( + isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, + isovalmax = is_list(isovalue) ? isovalue[1] : INF, + dumiso1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")), + dumiso2 = assert(isovalmin != -INF || isovalmin != INF, "\nIsovalue range must be finite on one end."), + exactbounds = is_def(exact_bounds) ? exact_bounds : is_list(f), + + // new voxel or bounding box centered around original, to fit whole voxels + bbox0 = is_num(bounding_box) + ? let(hb=0.5*bounding_box) [[-hb,-hb,-hb],[hb,hb,hb]] + : bounding_box, + autovoxsize = is_def(voxel_size) ? voxel_size : _getautovoxsize(bbox0, default(voxel_count,22^3)), + voxsize = _mball ? voxel_size : _getvoxsize(autovoxsize, bbox0, exactbounds), + bbox = _mball ? bounding_box : _getbbox(voxsize, bbox0, exactbounds, f), + // proceed with isosurface computations + cubes = _isosurface_cubes(voxsize, bbox, + fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef, + isovalmin=isovalmin, isovalmax=isovalmax, closed=closed), + tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, + tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, + trianglepoints = _isosurface_triangles(cubes, voxsize, isovalmin, isovalmax, tritablemin, tritablemax), + faces = [ + for(i=[0:3:len(trianglepoints)-1]) + let(i1=i+1, i2=i+2) + if (norm(cross(trianglepoints[i1]-trianglepoints[i], + trianglepoints[i2]-trianglepoints[i])) > EPSILON) + [i,i1,i2] + ], + dum2 = show_stats ? _showstats_isosurface(voxsize, bbox, isovalue, cubes, trianglepoints, faces) : 0 +) [trianglepoints, faces]; + +/// internal function: get voxel size given a desired number of voxels in a bounding box +function _getautovoxsize(bbox, numvoxels) = + let( + bbsiz = bbox[1]-bbox[0], + bbvol = bbsiz[0]*bbsiz[1]*bbsiz[2], + voxvol = bbvol/numvoxels + ) voxvol^(1/3); + +/// internal function: get voxel size, adjusted if necessary to fit bounding box +function _getvoxsize(voxel_size, bounding_box, exactbounds) = + let(voxsize0 = is_num(voxel_size) ? [voxel_size, voxel_size, voxel_size] : voxel_size) + exactbounds ? + let( + reqboxsize = bounding_box[1] - bounding_box[0], + bbnums = v_ceil(v_div(bounding_box[1]-bounding_box[0], voxsize0)), + newboxsize = v_mul(bbnums, voxsize0) + ) v_mul(voxsize0, v_div(reqboxsize, newboxsize)) + : voxsize0; // if exactbounds==false, we don't adjust voxel size + +/// internal function: get bounding box, adjusted in size and centered on requested box +function _getbbox(voxel_size, bounding_box, exactbounds, f=undef) = + let( + voxsize0 = is_num(voxel_size) ? [voxel_size, voxel_size, voxel_size] : voxel_size, + bbox = is_list(bounding_box) ? bounding_box + : is_num(bounding_box) ? let(hb=0.5*bounding_box) [[-hb,-hb,-hb],[hb,hb,hb]] + : let( // bounding_box==undef if we get here, then f must be an array + bbnums = [len(f), len(f[0]), len(f[0][0])] - [1,1,1], + halfbb = 0.5 * v_mul(voxsize0, bbnums) + ) [-halfbb, halfbb] + ) exactbounds ? + bbox // if grow_bounds==false, we don't adjust bounding box + : let( // adjust bounding box + bbcenter = mean(bbox), + bbnums = v_ceil(v_div(bbox[1]-bbox[0], voxsize0)), + halfbb = 0.5 * v_mul(voxsize0, bbnums) + ) [bbcenter - halfbb, bbcenter + halfbb]; + +/// _showstats_isosurface() (Private function) - called by isosurface() +/// Display statistics about isosurface +function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) = + let( + voxbounds = len(cubes)>0 ? let( + v = column(cubes, 0), // extract cube vertices + x = column(v,0), // extract x values + y = column(v,1), // extract y values + z = column(v,2), // extract z values + xmin = min(x), + xmax = max(x)+voxsize.x, + ymin = min(y), + ymax = max(y)+voxsize.y, + zmin = min(z), + zmax = max(z)+voxsize.z + ) [[xmin,ymin,zmin], [xmax,ymax,zmax]] : "N/A", + nvox = len(cubes), + ntri = len(triangles), + tribounds = ntri>0 ? pointlist_bounds(triangles) : "N/A" + ) echo(str("\nIsosurface statistics:\n Isovalue = ", isoval, "\n Voxel size = ", voxsize, + "\n Voxels intersected by the surface = ", nvox, + "\n Triangles = ", ntri, + "\n VNF bounds = ", tribounds, + "\n Bounds for all data = ", bbox, + "\n Voxel bounding box for isosurface = ", voxbounds, + "\n"));