1# Programming 2 3The Functional Geometry system (FG) provides both a low-level and a high-level API. 4* The high level API includes familiar operations like sphere(), translate() 5 and intersection(), 6 plus new operations like shell(), morph() and perimeter_extrude(). 7* The low level API allows users to directly define new primitive operations 8 using functional representation. 9 10The low level API is quite powerful. Most of the OpenSCAD geometry 11primitives can be defined in a few hundred lines of OpenSCAD2. 12That's very impressive, compared to the amount of code required to implement 13these operations on a mesh. 14The low level API is 15also subtle and tricky. So you can define `sphere()`, `union()`, etc, with 3 16lines of code per primitive, but they can be subtle and tricky lines of code. 17 18This document is a tutorial introduction to the low level API, 19which is illustrated by showing you how to define a lot of interesting 20high level operations. 21There's enough detail so that you can really wrap your head around 22the idioms of creating geometry using Functional Representation. 23 24## Benefits and Limitations 25 26When compared to the mesh-based programming model of OpenSCAD, 27the Functional Geometry API has both benefits and limitations. 28 29* **expressive power** <br> 30 A primary benefit of Functional Geometry is that you can define powerful new 31 geometric primitives with only a few lines of OpenSCAD2 code. 32 Most of OpenSCAD's geometry kernel can be implemented, quite simply, 33 at user level. By contrast, programming these same primitives using a mesh 34 is notoriously difficult, with lots of edge conditions to deal with, 35 and a lot of additional complexity if you use floating point (as opposed 36 to the slow and memory-inefficient rational numbers used by OpenSCAD). 37 38* **direct access to the representation of shapes** <br> 39 In OpenSCAD, shapes are opaque: you can't access their representation; 40 you can't even query the bounding box, let alone access the vertexes and faces. 41 This restriction is due to an engineering tradeoff: we want preview to be fast, 42 and we don't generate the mesh representation of CSG operations during preview. 43 If we were to provide access to the mesh during preview, then preview would become as slow as rendering. 44 Functional Geometry has no such tradeoff, and much of the programming power is due 45 to the fact that we provide full access to the underlying functional representation. 46 47* **curved surfaces** <br> 48 It's extremely easy to work with curved surfaces in Functional Geometry, 49 something that is a major weakness of OpenSCAD. 50 * A curved object is represented by its mathematical formula, 51 which makes curved surfaces and organic forms easy to define with very little code. 52 * Internally, a curved object is represented exactly. It's like programming in 53 OpenSCAD with `$fn=∞`. Curved objects can be exactly rendered in the preview window at full resolution, 54 even for complex models. You can zoom in many orders of magnitude without the abstraction 55 breaking down, limited only by floating point resolution. 56 By contrast, in OpenSCAD, curved surfaces are represented by polygonal 57 approximations that are chosen when the object is created, and errors accumulate as 58 these curved surfaces are further transformed. In OpenSCAD, you must find 59 a value of `$fn` that's high enough to achieve the print quality you want 60 while still low enough to prevent OpenSCAD from getting too slow or crashing while 61 you are working on the design. 62 63* **complex objects with micro-fine detail** <br> 64 Using Functional Geometry, it is possible to create models that have huge 65 amounts of procedurally generated detail, without making preview too slow 66 or exceeding your memory limit. For example, fractals or digital fabrics. 67 You can create complex models that would be impossible in OpenSCAD, because 68 too many triangles would be required. To accomplish this, you need to use 69 either the (new) high level spatial repetition and patterning operators, or the low level API, 70 since unioning a million objects is just as problematic in FG as it is in OpenSCAD. 71 72* **no "non-manifold objects"** <br> 73 The OpenSCAD rendering engine, based on CGAL, is very picky about "non-manifold objects", 74 so you have to use tricks to perturb your model in ways to avoid these errors. 75 The problem doesn't occur in preview mode, and it's not something you worry about 76 in Functional Geometry either. 77 78* **high level annoyances** <br> 79 Of course, Functional Geometry has its own limitations. 80 We will discover that the FG high level API has its own annoyances that affect 81 the programming model. 82 83* **low level annoyances** <br> 84 The FG low level API has an ease of use problem, similar to `polyhedron` in OpenSCAD. 85 It's possible to write a bad distance function for a primitive shape, 86 which could cause error messages or rendering problems later, and it's hard to 87 automatically detect and report these problems when the shape is constructed. 88 89* **no `minkowski` or `hull`** <br> 90 There's no efficient implementation of Minkowski Sum and Convex Hull. 91 Of course, we could convert functional representation to a mesh, and run the 92 mesh versions of these operations, but that's even slower than OpenSCAD. 93 Even in OpenSCAD, these are slow operations that kill preview performance. 94 Fortunately, there are good alternatives to the OpenSCAD idioms that 95 use these operations, which preview quickly. 96 97* **not a boundary representation** <br> 98 Functional representation does not directly represent the boundary of an object in the 99 same way that a mesh does. This may lead to compromises when what you really want is 100 direct control over the mesh, for example in STL export. It may require you to 101 make additional decisions to configure space/time/accuracy tradeoffs during mesh generation. 102 If necessary, I'll investigate a hybrid approach to mitigate this problem, 103 but using mesh features will take away from the simplicity of Functional Geometry programming. 104 105## Functional Representation (F-Rep) 106 107F-Rep (functional representation) is a leading alternative to B-Rep (boundary representation) 108as a general purpose representation for geometric objects. Examples of B-Rep include the 109OpenSCAD polyhedral mesh, and spline based representations. 110Like voxel representations, F-Rep is considered a volumetric representation. 111 112In F-Rep, a shape is represented by a distance field: 113a scalar field that specifies the minimum distance to a shape. 114The distance is signed: negative inside the shape, zero on the boundary, 115and positive outside the shape. 116 117A distance field is a function that maps every point in space (specified as [x,y,z]) 118to a distance value. A distance function can be derived from the mathematical equation 119for a shape. For example, a sphere of radius `r` has the equation 120``` 121x^2 + y^2 + z^2 = r^2 122``` 123This equation is true for all [x,y,z] points on the surface of the sphere. 124 125We can rewrite this as `x^2 + y^2 + z^2 - r^2 = 0`, 126and then derive a scalar field 127``` 128f[x,y,z] = x^2 + y^2 + z^2 - r^2 129``` 130which has the value zero for points on the surface of the sphere. 131 132To convert this to a distance field, we need one more transformation: 133``` 134f[x,y,z] = sqrt(x^2 + y^2 + z^2) - r 135``` 136 137### Constraints on Distance Functions 138We need to constrain distance functions so that the algorithms 139used to render shapes and export them to other data structures 140will still work. 141 142A distance function returns a *minimum distance* `d` 143between the point argument and the nearest object boundary. 144* If `d` is 0, 145 then the point must be on an object boundary. 146* Otherwise, the nearest object boundary 147 must be at least `abs(d)` units away, 148 but it could be farther. There is room for flexibility 149 because of isosurfaces, discussed below. 150* If this constraint is violated, then the ray-marching algorithm 151 for directly rendering a shape on a GPU will not work. 152 153A distance function must be [strictly increasing](http://mathworld.wolfram.com/StrictlyIncreasingFunction.html) 154(for points outside of objects) or strictly decreasing 155(for points inside of objects) as the point gets 156increasingly farther away from the nearest object boundary. 157This paragraph is speculative. 158 159### Isosurfaces 160The isosurface at distance d of a distance field f 161is the set of all points p such that f(p)=d. 162The isosurface at distance 0 is the boundary of the shape. 163The isosurface at distance 1 is another boundary, a shell 164that encloses the shape, and is separated from it by a minimum distance of 1. 165Isosurfaces are important because they are used to compute shells. 166 167When you design a distance function, you aren't just specifying 168the boundary of the shape, you are also specifying an infinite family 169of isosurfaces that enclose or are enclosed by the shape. 170It's important to consider what all of these isosurfaces look like, 171because that will determine what shells look like, as computed by 172the `shell` and `inflate` operations. 173 174### Infinite Shapes 175Distance fields are not restricted to representing finite geometrical objects. 176They can also represent infinite space-filling patterns. 177For examples, try a Google image search on 178[k3dsurf periodic lattice](https://www.google.ca/search?q=k3dsurf+periodic+lattice&tbm=isch). 179These infinite patterns are useful in 3D modelling: 180you can intersect them or subtract them from a finite 3D object. 181 182### Materials and Colours 183A material field or a colour field can be associated with a shape, 184assigning a material or colour to every point within the volume of a shape. 185This is an aspect of F-Rep that will be added to FG in a later revision. 186 187### Other Resources 188 189This topic is hard to google, because so many different terms are used. 190Try F-Rep, FRep, functional representation, 191On the Representation of Shapes Using Implicit Functions, 192distance field, distance function, signed distance function, 193isosurface, procedural modeling. 194 195Try this essay on 196[the mathematical basis of F-Rep](https://christopherolah.wordpress.com/2011/11/06/manipulation-of-implicit-functions-with-an-eye-on-cad/) 197by Christopher Olah, inventor of ImplicitCAD. 198 199## The Representation of Shapes in FG 200In the Functional Geometry system, every shape has two fields: 201* `dist`, a distance function. 202* `bbox`, a function which maps a distance value `d` 203 onto the bounding box of the isosurface of `d`. 204 205A bounding box has the representation `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`. 206A bounding box may be larger than is strictly necessary to enclose the 207isosurface, but it must not be smaller. This flexibility is permitted 208due to the difficulty in computing an exact bounding box for some shapes. 209 210Empty shapes are supported, but the bounding box must also be empty. 211A bounding box has zero width along the X axis if xmin >= xmax, 212which means the bounding box is empty. Likewise for the Y and Z axes. 213 214A bbox function should handle a distance argument outside the range of 215its associated dist function by returning the empty bounding box. 216This can happen when computing shells. If you are lucky, then an out of range 217distance argument will result in your bbox function returning a bounding box 218where xmin > xmax, or ymin > ymax, or zmin > zmax, just because the arithmetic 219works out that way, and not because you wrote special case code. 220 221Both finite and infinite shapes are supported. 222If a shape extends to infinity along any of the 6 cardinal directions, 223then this is indicated in the bounding box by setting xmin, ymin or zmin 224to -inf, or by setting xmax, ymax or zmax to inf. 225 226A distance field can, in principle, specify a degenerate shape that has zero thickness 227across some or all of its extension. Which classes of degenerate shapes that we 228actually support is an open question. We use 64 bit IEEE floats for geometry 229computations, so shapes could become degenerate as a result of floating point underflow. 230This suggests that the FG geometry engine should be tolerant of degeneracy. 231 232(In OpenSCAD, a mesh computation with valid inputs can produce a non-manifold 233output, even though CGAL is being used throughout. And then you get CGAL 234exceptions. It's the same for all other mesh libraries. How hard is it to make 235FG robust against this kind of problem?) 236 237### Rationale 238Why do we need to specify a bounding box? 239Because the geometry kernel needs it to render STL, and to pick a bounding box 240for the preview window. 241 242There are two kinds of F-Rep geometry systems: 243* Systems like Hyperfun or ShapeJS require the user to specify a global bounding 244 box for all the geometry in a project. But primitive operations can be coded 245 without specifying a bounding box, since that's the user's problem. 246* Systems like ImplicitCAD, Antimony and FG automatically compute a bounding box 247 for each shape and operator, so that the user doesn't have to, but this moves 248 the complexity into the definition of each primitive. 249 250Why do we need to specify a bounding box *function*, instead of just a single 251bounding box for the isosurface at value 0? 252Because it's required by `inflate`, and other operations that compute shells. 253In the general case, you can't predict the bounding box of a shell from 254the bounding box of the isosurface at 0. A few primitives 255have "weird" isosurfaces with unusual bounding boxes, including 256anisotropic `scale`. 257* NOTE: I've changed my mind. Inflate will now assert that its input needs to 258 be a Euclidean distance field, and primitives lower in the tree will switch 259 to a more expensive algorithm (if required) to ensure this. 260 261## Low Level API 262 263`3dshape(dist([x,y,z])=..., bbox(d)=...)` 264> Returns a functional 3D shape, specified by a distance function 265> and a bounding box function. 266 267`2dshape(dist([x,y])=..., bbox(d)=...)` 268> Returns a functional 2D shape, specified by a distance function 269> and a bounding box function. 270 271The low level API contains utility operations that are used 272to define new operations that map shapes onto shapes. 273Shapes can be queried at run-time for their distance 274and bounding box functions. 275 276`shape.dist` 277> the distance function of a shape 278 279`shape.bbox` 280> the bounding box function of a shape 281 282## Circle and Sphere 283A circle of radius `r`, centred on the origin: 284``` 285circle(r) = 2dshape( 286 dist(p) = norm(p) - r, 287 bbox(d)=[[-r,-r]-d,[r,r]+d]); 288``` 289 290New style: 291``` 292circle(r:is_num) = make_shape{ 293 dist(req)(p:is_vec3) = norm(p) - r, 294 bbox=[[-r,-r],[r,r]]}; 295``` 296 297The `dist` function is derived from 298[the mathematical equation for a circle](https://en.wikipedia.org/wiki/Circle#Equations): 299``` 300 x^2 + y^2 = r^2 301-> sqrt(x^2 + y^2) = r 302-> sqrt(x^2 + y^2) - r = 0 303-> dist[x,y] = sqrt(x^2 + y^2) - r 304-> dist(p) = norm(p) - r 305``` 306This particular `dist` function returns the shortest euclidean distance 307from the point to the perimeter of the circle. 308 309Every `dist` function defines an infinite family of isosurfaces, 310or in the 2D case, isolines. 311For this particular `dist` function, all of the isolines are circles, 312so the shell of a circle is another circle. 313 314Most primitive operations, including `circle`, have isosurfaces that behave in a standard way: 315the bbox expands outwards by 1 unit for each unit increase in d. 316For these operations, the bbox function has the form: 317``` 318bbox(d) = [[...]-d, [...]+d] 319``` 320 321The range of our `dist` function is infinity to `-r`. 322 323A `bbox` function is required to return the empty bbox 324if it is passed an argument outside the range of the `dist` function. 325In this case, the math works out so that this happens automatically, 326without using an `if` expression. Eg, 327``` 328 c = circle(2); 329 c.bbox(-3) == [[1,1],[-1,-1]] 330``` 331And this bbox is empty since xmin > xmax, and ymin > ymax. 332 333The definition of `sphere` is very similar: 334``` 335sphere(r) = 3dshape( 336 dist(p) = norm(p) - r, 337 bbox(d)=[[-r,-r,-r]-d,[r,r,r]+d]); 338``` 339 340## CSG Operations 341``` 342union(s1,s2) = 3dshape( 343 dist(p) = min(s1.dist(p), s2.dist(p)), 344 bbox(d)=[ min(s1.bbox(d)[0],s2.bbox(d)[0]), max(s1.bbox(d)[1],s2.bbox(d)[1]) ]); 345``` 346``` 347intersection(s1,s2) = 3dshape( 348 dist(p) = max(s1.dist(p), s2.dist(p)), 349 bbox(d)=[ max(s1.bbox(d)[0],s2.bbox(d)[0]), min(s1.bbox(d)[1],s2.bbox(d)[1]) ]); 350``` 351Union and intersection are implemented as the minimum and maximum 352of the shape argument distance functions. This computes the union or 353intersection of all of the isosurfaces in the distance functions. 354Whenever you see `min` or `max` of a distance, in a distance function, 355a union or intersection is being computed. 356Also, it's likely that a sharp angle is being introduced into the shape. 357 358``` 359complement(s) = 3dshape( 360 dist(p) = -s.dist(p), 361 bbox(d) = [[-inf,-inf,-inf],[inf,inf,inf]] ); 362``` 363The `complement` operation reverses the inside and outside of a shape. 364The result is infinite. 365It's useful when working with infinite space filling patterns, 366and for defining `difference`. 367 368``` 369difference(a,b) = intersection(a, complement(b)); 370``` 371Subtract `b` from `a`. 372 373## Rectangle and Cuboid 374 375Consider an axis-aligned rectangle, specified by `[[xmin,ymin],[xmax,ymax]]`. 376This is a convex 4-sided polygon, which we can compute by 377[intersecting 4 half-planes](https://en.wikipedia.org/wiki/Convex_polytope#Intersection_of_half-spaces). 378 379Consider the half-plane containing all points such that x >= xmin. 380Rewrite this as xmin <= x. 381The corresponding distance function is `xmin - x`. 382 383Here are the 4 half-planes, and their distance functions: 384``` 385x >= xmin xmin - x 386x <= xmax x - xmax 387y >= ymin ymin - y 388y <= ymax y - ymax 389``` 390Here is their intersection: 391``` 392max(xmin - x, x - xmax, ymin - y, y - ymax) 393``` 394And here's the code: 395``` 396box2([[xmin,ymin],[xmax,ymax]]) = 2dshape( 397 dist([x,y]) = max(xmin - x, x - xmax, ymin - y, y - ymax), 398 bbox(d)=[ [xmin,ymin]-d, [xmax,ymax]+d ]); 399``` 400 401Now let's design a centred rectangle primitive, where the width and height are specified by `[dx,dy]`. 402 403The distance function is `max(-dx/2 - x, x - dx/2, -dy/2 - y, y - dy/2)`. 404 405We can simplify and speed it up: `max(abs(x) - dx/2, abs(y) - dy/2)`, 406exploiting the fact that the distance is symmetric around the origin. 407 408Here's the code: 409``` 410rectangle([dx,dy]) = 2dshape( 411 dist([x,y,z]) = max(abs(x) - dx/2, abs(y) - dy/2), 412 bbox(d)=[ [-dx/2,-dy/2]-d, [dx/2,dy/2]+d ]); 413``` 414 415Cuboid is similar; conceptually it works by 416[intersecting 6 half-spaces to make the 6 faces of the cuboid](https://en.wikipedia.org/wiki/Convex_polytope#Intersection_of_half-spaces): 417``` 418cuboid([dx,dy,dz]) = 3dshape( 419 dist([x,y,z]) = max(abs(x)-dx/2, abs(y)-dy/2, abs(z)-dz/2), 420 bbox(d)=[ [-dx/2,-dy/2,-dz/2]-d, [dx/2,dy/2,dz/2]+d ]); 421``` 422 423This technique can be generalized to model any convex polygon/polyhedron, 424by intersecting one half-plane/half-space for each edge/face. 425It can be further generalized to non-convex polytopes by using 426[Nef polygons/polyhedra](https://en.wikipedia.org/wiki/Nef_polygon), 427where both intersection and complement operations are used. 428 429However, this technique requires each edge/face to be examined in each 430call to the distance function. It will become inefficient for sufficently large 431numbers of edges/faces; for that, another approach should be found. 432 433## Cylinder and Linear_extrude 434 435We will construct a centred cylinder by intersecting 3 infinite shapes: 436two half-spaces (one for each end cap), and an infinite cylinder 437for the body. This is similar to the construction of a cuboid 438from six half-spaces. 439 440We'll first construct an infinite cylinder centred on the Z-axis. 441To do this, we just take the distance function for a centred circle, 442and put that into a 3D distance function: 443``` 444dist([x,y,z]) = norm([x,y]) - r 445``` 446We get infinite extension along the Z axis by ignoring the `z` parameter. 447This way, every slice through the distance field that is parallel to the X-Y plane 448looks exactly the same. 449 450The two half-spaces follow from our construction for cuboid. 451``` 452z <= h/2 z - h/2 453z >= -h/2 -h/2 - z 454``` 455The intersection of these three infinite solids is 456``` 457max(norm([x,y]) - r, z - h/2, -h/2 - z) 458``` 459We'll apply the same optimization for a centred object that we used for cuboid: 460``` 461max(norm([x,y]) - r, abs(z) - h/2) 462``` 463And here's the code: 464``` 465cylinder(h,r) = 3dshape( 466 dist([x,y,z]) = max(norm([x,y]) - r, abs(z) - h/2), 467 bbox(d)=[ [-r,-r,-h/2]-d, [r,r,h/2]+d ]); 468``` 469This can be generalized to `linear_extrude`: 470``` 471linear_extrude(h)(s) = 3dshape( 472 dist([x,y,z]) = max(s.dist([x,y]), abs(z) - h/2), 473 bbox(d)=[ [s.bbox(d)[0].x, s.bbox(d)[0].y, -h/2-d], 474 [s.bbox(d)[1].x, s.bbox(d)[1].y, h/2+d] ]); 475``` 476Now cylinder can be defined like this: 477``` 478cylinder(h,r) = linear_extrude(h) circle(r); 479``` 480 481## Rotate_extrude 482 483(Torus from Olah's essay): 484We can use the output of one of these functions as the input for another. For example, if we feed r = \sqrt{x^2+y^2} - k_1, the outwardness of a circle on the x,y-plane, into a new circle which we can think of as being on the r,z-plane, \sqrt{r^2+z^2}-k_2, we get a torus. 485That is, \sqrt{(\sqrt{x^2+y^2} - k_1)^2+z^2}-k_2 = 0 results in a torus. 486 487 488rotate_extrude from ImplicitCAD 489``` 490getImplicit3 (RotateExtrude totalRotation round translate rotate symbObj) = 491 let 492 tau = 2 * pi 493 k = tau / 360 494 totalRotation' = totalRotation*k 495 obj = getImplicit2 symbObj 496 capped = Maybe.isJust round 497 round' = Maybe.fromMaybe 0 round 498 translate' :: ℝ -> ℝ2 499 translate' = Either.either 500 (\(a,b) -> \θ -> (a*θ/totalRotation', b*θ/totalRotation')) 501 (. (/k)) 502 translate 503 rotate' :: ℝ -> ℝ 504 rotate' = Either.either 505 (\t -> \θ -> t*θ/totalRotation' ) 506 (. (/k)) 507 rotate 508 twists = case rotate of 509 Left 0 -> True 510 _ -> False 511 in 512 \(x,y,z) -> minimum $ do 513 514 let 515 r = sqrt (x^2 + y^2) 516 θ = atan2 y x 517 ns :: [Int] 518 ns = 519 if capped 520 then -- we will cap a different way, but want leeway to keep the function cont 521 [-1 .. (ceiling (totalRotation' / tau) :: Int) + (1 :: Int)] 522 else 523 [0 .. floor $ (totalRotation' - θ) /tau] 524 n <- ns 525 let 526 θvirt = fromIntegral n * tau + θ 527 (rshift, zshift) = translate' θvirt 528 twist = rotate' θvirt 529 rz_pos = if twists 530 then let 531 (c,s) = (cos(twist*k), sin(twist*k)) 532 (r',z') = (r-rshift, z-zshift) 533 in 534 (c*r' - s*z', c*z' + s*r') 535 else (r - rshift, z - zshift) 536 return $ 537 if capped 538 then MathUtil.rmax round' 539 (abs (θvirt - (totalRotation' / 2)) - (totalRotation' / 2)) 540 (obj rz_pos) 541 else obj rz_pos 542``` 543 544ExtrudeOnEdgeOf from ImplicitCAD 545``` 546getImplicit3 (ExtrudeOnEdgeOf symbObj1 symbObj2) = 547 let 548 obj1 = getImplicit2 symbObj1 549 obj2 = getImplicit2 symbObj2 550 in 551 \(x,y,z) -> obj1 (obj2 (x,y), z) 552getBox3 (ExtrudeOnEdgeOf symbObj1 symbObj2) = 553 let 554 ((ax1,ay1),(ax2,ay2)) = getBox2 symbObj1 555 ((bx1,by1),(bx2,by2)) = getBox2 symbObj2 556 in 557 ((bx1+ax1, by1+ax1, ay1), (bx2+ax2, by2+ax2, ay2)) 558``` 559``` 560perimeter_extrude(a,b) = 3dshape( 561 dist([x,y,z]) = a.dist([b.dist([x,y]), z]), 562 bbox(d)=let ([[ax1,ay1],[ax2,ay2]] = a.bbox(d), 563 [[bx1,by1],[bx2,by2]] = b.bbox(d)) 564 [[bx1+ax1, by1+ax1, ay1], [bx2+ax2, by2+ax2, ay2]] ); 565``` 566Extrude 2D object `a` around the perimeter of 2D object `b` to get a 3D object. 567The perimeter of 'b' lies on the X-Y plane. The (0,0) point of `a` is aligned with 568the perimeter as `a` is swept around the perimeter. 569 570If `a` and `b` are both circles, then you get a torus. 571 572This is ExtrudeOnEdgeOf from ImplicitCAD. 573Based on a few rendered examples, figure `a` is normal to the perimeter at each 574point on the perimeter, which is what you want. But how does it work? 575 576``` 577rotate_extrude(r)(s) = perimeter_extrude(s, circle(r)); 578torus(r1,r2) = rotate_extrude(r1) circle(r2); 579``` 580 581## Shells 582 583There are many different ways to write a distance function that produce the same visible result. 584The main requirement is that the isosurface of the distance function at 0 585(the set of all points such that f[x,y,z] == 0) 586is the boundary of the desired object, and there are infinitely many functions that satisfy this 587requirement for any given shape. 588 589But the isosurface at 0 is not the only thing you need to consider. 590A distance function defines an infinite family of isosurfaces, 591and they are all important, particular when you use the `shell` primitive. 592Consider 593``` 594c = cuboid([10,20,30]); 595``` 596`c.dist` is the distance function. Because of the way we defined `cuboid`, 597the isosurface of `c.dist` at -1 is cuboid(8,18,28]), the isosurface at 1 598is cuboid([12,22,32]), and so on. All of the isosurfaces of `c` are cuboids, 599although with varying aspect ratios. 600As a result, `shell(1) sp` will return a spherical shell with outer radius 10 601and inner radius 9. The `shell` operator provides direct access to the other isosurfaces. 602(See the Shell section, which is later.) 603 604I have similar comments for `cuboid`. 605The distance function is designed so that all of the isosurfaces of a given 606cuboid are nested cuboids, and `shell` works as expected. 607 608Here's a thought experiment. Consider defining the distance function for `cuboid` 609so the the distance to the boundary is measured in millimeters along a vector that points to the centre. 610With this definition, only the isosurface at 0 would be a true cuboid. The isosurfaces at positive values 611would have increasingly convex sides, so that at large values, the surface would approach a sphere. 612The isosurfaces at negative values would have increasingly concave sides. 613This behaviour would be visible using `shell`. 614 615 616## Primitive Shapes 617Here I'll define 3 primitive shapes: sphere, cuboid and cylinder. 618The main lesson will be learning how to write a distance function. 619 620``` 621sphere(r) = 3dshape( 622 f([x,y,z]) = sqrt(x^2 + y^2 + z^2) - r, 623 bbox=[[-r,-r,-r],[r,r,r]]); 624``` 625 626In the F-Rep section, I said that `f([x,y,z]) = x^2 + y^2 + z^2 - r^2` 627is a distance function for a sphere, so why did we change it? 628 629There are many different ways to write a distance function that produce the same visible result. 630The main requirement is that the isosurface of the distance function at 0 631(the set of all points such that f[x,y,z] == 0) 632is the boundary of the desired object, and there are infinitely many functions that satisfy this 633requirement for any given shape. 634 635But the isosurface at 0 is not the only thing you need to consider. 636A distance function defines an infinite family of isosurfaces, 637and they are all important, particular when you use the `shell` primitive. 638Consider 639``` 640sp = sphere(10); 641``` 642`sp.f` is the distance function. Because of the way we defined `sphere`, 643the isosurface of `sp.f` at -1 is a sphere of radius 9, the isosurface at 1 644is a sphere of radius 11, and so on. All of the isosurfaces of `sp` are spheres. 645As a result, `shell(1) sp` will return a spherical shell with outer radius 10 646and inner radius 9. The `shell` operator provides direct access to the other isosurfaces. 647(See the Shell section, which is later.) 648 649``` 650cuboid(sz) = 3dshape( 651 f([x,y,z]) = max([abs(x-sz.x/2), abs(y-sz.y/2), abs(z-sz.z/2)]), 652 bbox=[ [0,0,0], sz ]); 653``` 654I have similar comments for `cuboid`. 655The distance function is designed so that all of the isosurfaces of a given 656cuboid are nested cuboids, and `shell` works as expected. 657 658Here's a thought experiment. Consider defining the distance function for `cuboid` 659so the the distance to the boundary is measured in millimeters along a vector that points to the centre. 660With this definition, only the isosurface at 0 would be a true cuboid. The isosurfaces at positive values 661would have increasingly convex sides, so that at large values, the surface would approach a sphere. 662The isosurfaces at negative values would have increasingly concave sides. 663This behaviour would be visible using `shell`. 664 665Note the use of `max` in the distance function for `cuboid`. 666Generally speaking, if your distance function computes the max of a function of `x` and a function of `y`, 667then your shape will have a right angle between the X and Y axes. 668Read [Olah's essay](https://christopherolah.wordpress.com/2011/11/06/manipulation-of-implicit-functions-with-an-eye-on-cad/) for more discussion of this. 669 670``` 671cylinder(h,r) = linear_extrude(h=h) circle(r); 672``` 673 674``` 675circle(r) = 2dshape( 676 f([x,y]) = sqrt(x^2 + y^2) - r, 677 bbox=[[-r,-r],[r,r]]); 678``` 679 680``` 681linear_extrude(h)(shape) = 3dshape( 682 f([x,y,z]) = max(shape.f(x,y), abs(z - h/2) - h/2), 683 bbox=[ [shape.bbox[0].x,shape.bbox[0].y,-h/2], [shape.bbox[1].x,shape.bbox[1].y,h/2] ]); 684``` 685> The result of `linear_extrude` is centred. 686 687The distance function for `linear_extrude` computes the max 688of a function of x & y with a function of z, 689and the result is a right angle between the X/Y plane and the Z axis. 690See above. 691 692## Translation 693translate(), align(), pack???. 694No need for centre= options. 695 696## Shells and Isosurfaces 697 698### `shell(n) shape` 699hollows out the specified shape, leaving only a shell of the specified 700thickness. 701An analogy is 3D printing a shape without infill. 702 703``` 704shell(w)(shape) = 3dshape( 705 f(p) = abs(shape.f(p) + w/2) - w/2, 706 bbox = shape.bbox ); 707``` 708Explanation: 709* `f(p) = abs(shape.f(p))` is the zero-thickness shell of `shape`. 710* `f(p) = abs(shape.f(p)) - n` (for positive n) is a shell of thickness `n*2`, 711 centred on the zero-thickness shell given above. 712* `f(p) = shape.f(p) + n` (for positive n) is the isosurface of `shape` at `-n` 713 (it's smaller than `shape`) 714 715### `inflate(n) shape` 716returns the isosurface of `shape` at value `n`. 717Positive values of `n` create a larger version of `shape`, 718`n==0` is the identity transformation, 719and negative values "deflate" the shape. 720This is different from the scale transformation; it's more like 721inflating a shape like a balloon, especially for non-convex objects. 722 723![inflate](img/inflate.png) 724<br>[image source](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf) 725 726``` 727inflate(n)(shape) = 3dshape( 728 f(p) = shape.f(p) - n, 729 bbox=[ shape.bbox[0]-n, shape.bbox[1]+n ]); 730``` 731TODO: is the bbox calculation correct in all cases? No, not for anisotropic scaling. 732The bbox could be bigger than this. 733Use f to compute the bbox from shape.bbox using ray-marching. 734 735## Scaling 736Scaling is tricky. Several functional geometry systems get it wrong? 737 738### `isoscale(s) shape` 739This is an isotropic scaling operation: 740it scales a shape by the same factor `s` on all 3 axes, 741where `s` is a positive number. 742The code is easier to understand than for anisotropic scaling. 743 744``` 745isoscale(s)(shape) = 3dshape( 746 f(p) = s * shape.f(p / s), 747 bbox = s * shape.bbox 748); 749``` 750 751Suppose that the distance function 752was instead `f(p)=shape.f(p/s)`, 753as it is in Antimony. We don't multiply the result by `s`. 754This is good enough to scale the isosurface at 0, 755which means the scaled shape will render correctly. 756But the other isosurfaces will be messed up. Why? 757* Suppose s > 1. Eg, s==2 and we are scaling a centred sphere with radius 3. 758 We want the result to be a sphere with radius 6. 759 If we pass in p=[6,0,0], that's converted to p/s = [3,0,0] before passed to the sphere's distance function, 760 which then returns 0, indicating that p is on the boundary. Good. 761 If we pass in p=[8,0,0], that's converted to p/s = [4,0,0], then shape.f(p/s) returns 1. 762 This indicates we are a minimum of 1 mm from the boundary, which is satisfies 763 the ray-march property. However, the inflate property is not satisfied, since the 764 bounding box returned by `inflate(1)scale(2)sphere(3)` will be too small. 765* Suppose s < 1. 766 The ray-march property fails, but the inflate property is satisfied. 767 768 769### Negative Scale Factor 770In OpenSCAD, a negative scaling factor passed to the scale() operator 771will cause a reflection about the corresponding axis. This isn't documented, 772but it is a natural consequence of how scaling is implemented by an affine transformation matrix. 773 774The scale operators discussed in this document don't work that way: a negative scaling factor 775results in garbage. 776 777### `scale([sx,sy,sz]) shape` 778Anisotropic scaling is hard ... 779 780I don't see a way to implement anisotropic scaling in a way 781that satisfies both the ray-march and the inflate properties. 782The system needs to change. 783 784the effect of anisotropic scaling on `shell`. Need for `spheroid`. 785 786``` 787getImplicit3 (Scale3 s@(sx,sy,sz) symbObj) = 788 let 789 obj = getImplicit3 symbObj 790 k = (sx*sy*sz)**(1/3) 791 in 792 \p -> k * obj (p ⋯/ s) 793getBox3 (Scale3 s symbObj) = 794 let 795 (a,b) = getBox3 symbObj 796 in 797 (s ⋯* a, s ⋯* b) 798``` 799 800``` 801scale(s)(shape) = 3dshape( 802 f(p) = (s.x*s.y*s.z)^(1/3) * shape.f(p / s), 803 bbox = s * shape.bbox 804); 805``` 806 807explanation: 808* `f0(p) = shape.f(p/s)` correctly scales the isosurface at 0, 809 so the shape will render correctly. 810 But the other isosurfaces will be messed up, 811 because the distances between isosurfaces will also be scaled. 812 So `f0` isn't a valid distance function: 813 it will cause `shell` and `inflate` to not work, in some cases. 814* ImplicitCAD multiplies f0 by k=cube_root(s.x * s.y * s.z) 815 to get a valid distance function. I don't understand. 816 Suppose s=[27,1,1]. Then k=3. How can this be correct? 817* Antimony just uses `f0`; it doesn't even try to fix the other 818 isosurfaces. I don't think the correct code is even expressible. 819 820Antimony: 821``` 822def scale_x(part, x0, sx): 823 # X' = x0 + (X-x0)/sx 824 return part.map(Transform( 825 '+f%(x0)g/-Xf%(x0)gf%(sx)g' % locals() 826 if x0 else '/Xf%g' % sx, 827 'Y', 828 '+f%(x0)g*f%(sx)g-Xf%(x0)g' % locals() 829 if x0 else '*Xf%g' % sx, 830 'Y')) 831def scale_xyz(part, x0, y0, z0, sx, sy, sz): 832 # X' = x0 + (X-x0)/sx 833 # Y' = y0 + (Y-y0)/sy 834 # Z' = z0 + (Z-z0)/sz 835 # X = x0 + (X'-x0)*sx 836 # Y = y0 + (Y'-y0)*sy 837 # Z = z0 + (Z'-z0)*sz 838 return part.map(Transform( 839 '+f%(x0)g/-Xf%(x0)gf%(sx)g' % locals(), 840 '+f%(y0)g/-Yf%(y0)gf%(sy)g' % locals(), 841 '+f%(z0)g/-Zf%(z0)gf%(sz)g' % locals(), 842 '+f%(x0)g*-Xf%(x0)gf%(sx)g' % locals(), 843 '+f%(y0)g*-Yf%(y0)gf%(sy)g' % locals(), 844 '+f%(z0)g*-Zf%(z0)gf%(sz)g' % locals())) 845Shape Shape::map(Transform t) const 846{ 847 return Shape("m" + (t.x_forward.length() ? t.x_forward : "_") 848 + (t.y_forward.length() ? t.y_forward : "_") 849 + (t.z_forward.length() ? t.z_forward : "_") + math, 850 bounds.map(t)); 851} 852``` 853The scale functions also do translation, so you can scale an object locally without moving its origin point. 854With the translation removed, we get 855``` 856 # X' = X/sx 857 # X = X'*sx 858``` 859Antimony gets it wrong. It doesn't fix up the 860 861## CSG Operations 862everything, nothing, complement, 863union, intersection, difference 864 865``` 866complement(s) = 3dshape( 867 f(p) = -s.f(p) 868 bbox = [[-inf,-inf,-inf],[inf,inf,inf]] ); 869``` 870> Convert a shape or pattern to its inverse: all points inside the object 871> are now outside, and vice versa. 872 873meaning of 'max' and 'min' in a distance function 874 875Note: ImplicitCAD and Antimony use max for union and min for intersection. 876Although this is a common implementation, [Schmitt 2002] 877(http://grenet.drimm.u-bordeaux1.fr/pdf/2002/SCHMITT_BENJAMIN_2002.pdf) 878says "it is well known that using min/max functions for set-theoretic 879operations causes problems in further transformations of the model due to C1 discontinuity of 880the resulting function (see Fig. 1.3). Further blending, offsetting, or metamorphosis can result 881in unnecessary creases, edges, and tearing of such an object." (page 26, and figure 1.3 on page 20). 882His solution is to use an alternative function, one which is differentiable. 883My interpretation is that you need to round any sharp edges and add fillets to sharp corners 884to avoid this alleged problem. I'm going to wait and see if we can actually detect a problem 885once we have working code, before worrying about a fix. 886 887## Rounded edges and Fillets 888rcuboid, runion 889 890``` 891rcuboid(r,sz) = 3dshape( 892 f([x,y,z]) = rmax(r, [abs(x-sz.x/2), abs(y-sz.y/2), abs(z-sz.z/2)]), 893 bbox=[ [0,0,0], sz ]); 894``` 895> Cuboid with rounded corners (radius=r) from ImplicitCAD. 896> All ImplicitCAD primitives that generate sharp corners have a rounding option. 897> `rmax` is a utility function for creating rounded corners. 898 899``` 900runion(r,s1,s2) = 3dshape( 901 f(p) = rmin(r, s1.f(p), s2.f(p)), 902 bbox=[ min(s1.bbox[0],s2.bbox[0])-r, max(s1.bbox[1],s2.bbox[1])+r ]); 903``` 904> Create fillets! ImplicitCAD rounded union with radius `r`. (Why is the bbox padded by r?) 905Example: 906 ``` 907 runion(r=8, 908 cube([40,40,20]), 909 translate([0,0,20]) cube([20,20,30])) 910 ``` 911![rounded union](http://thingiverse-rerender-new.s3.amazonaws.com/renders/47/9e/1c/c8/e2/RoundedUnionCubeExample_preview_featured.jpg) 912 913## Perimeter_extrude 914 915## Infinite Patterns 916gyroid, infinite replication grid, etc 917 918## Morphing 919"Distance fields are also commonly used for 920shape metamorphosis. Compared to mesh-based morphing 921techniques, volume-based morphing is easier to implement 922and can naturally morph surfaces of different genus." 923[[Bastos 2008](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)]. 924 925Here's a simple morph operator based on linear interpolation 926of the distance functions: 927 928![morph](img/morph.png) 929<br>[image source](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf) 930 931``` 932morph(r,s1,s2) = 3dshape( 933 f(p) = interp(r, s1.f(p), s2,f(p)), 934 bbox = interp(r, s1.bbox, s2.bbox) 935); 936interp(r,a,b) = a + (b - a)*r; 937``` 938`r` normally ranges from 0 to 1, where 0 returns s1, 1 returns s2. 939But values <0 or >1 can also be used. 940 941"Unfortunately, in most cases simple interpolation is 942unable to provide convincing metamorphoses, as it does not 943preserve or blend corresponding shape features. In order to 944improve morphing quality, we could use a warp function 945to guide the distance field interpolation, as proposed by 946Cohen-Or 'Three dimensional distance field metamorphosis'" 947[[Bastos 2008](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)]. 948