├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── .gitignore ├── images │ ├── bend.png │ ├── bend_linear.png │ ├── bend_radial.png │ ├── blend.png │ ├── blobby.jpg │ ├── blobby.png │ ├── box.png │ ├── box2.png │ ├── capped_cone.png │ ├── capped_cylinder.png │ ├── capsule.png │ ├── circular_array.png │ ├── customizable_box.png │ ├── customizable_lid.png │ ├── cylinder.png │ ├── difference.png │ ├── dilate.png │ ├── dodecahedron.png │ ├── ellipsoid.png │ ├── elongate.png │ ├── erode.png │ ├── example.jpg │ ├── example.png │ ├── extrude.png │ ├── extrude_to.png │ ├── gearlike.jpg │ ├── gearlike.png │ ├── icosahedron.png │ ├── intersection.png │ ├── knurling.jpg │ ├── knurling.png │ ├── meshview.png │ ├── octahedron.png │ ├── orient.png │ ├── plane.png │ ├── pyramid.png │ ├── repeat.png │ ├── revolve.png │ ├── rotate.png │ ├── rounded_box.png │ ├── rounded_cone.png │ ├── rounded_cylinder.png │ ├── scale.png │ ├── shell.png │ ├── show_slice.png │ ├── slab.png │ ├── slice.png │ ├── smooth_difference.png │ ├── smooth_intersection.png │ ├── smooth_union.png │ ├── sphere.png │ ├── tetrahedron.png │ ├── text-large.png │ ├── text.png │ ├── torus.png │ ├── transition_linear.png │ ├── transition_radial.png │ ├── translate.png │ ├── twist.png │ ├── union.png │ ├── weave.jpg │ ├── weave.png │ ├── wireframe_box.png │ └── wrap_around.png ├── render.go └── render.py ├── examples ├── blobby.py ├── customizable_box.py ├── example.py ├── gearlike.py ├── knurling.py ├── pawn.py ├── text.py └── weave.py ├── sdf ├── __init__.py ├── d2.py ├── d3.py ├── dn.py ├── ease.py ├── mesh.py ├── progress.py ├── stl.py ├── text.py ├── torch_util.py └── util.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /sdf.egg-info 2 | *.stl 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdf-torch 2 | 3 | This is a fork of [fogleman/sdf](https://github.com/fogleman/sdf) that uses PyTorch instead of numpy for batched computation. I was curious if a GPU could greatly speed up SDF evaluation. Initial benchmarks show that torch+GPU can give a significant speedup over numpy+CPU, and even torch+CPU gives a (smaller) speedup. 4 | 5 | Here are some timings from a few example scripts: 6 | 7 | | Example | numpy | torch, CPU | torch, GPU | 8 | |----------|-------|------------|------------| 9 | | blobby | 10.9 | 9.5s | **9.2s** | 10 | | gearlike | 15.6 | 9.9s | **7.8s** | 11 | | weave | 27.6 | 11.9s | **3.7s** | 12 | 13 | **Benchmark hardware:** one `Titan X (Pascal)` and an 8 hypercore `Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz`. 14 | 15 | # Old README 16 | 17 | Generate 3D meshes based on SDFs (signed distance functions) with a 18 | dirt simple Python API. 19 | 20 | Special thanks to [Inigo Quilez](https://iquilezles.org/) for his excellent documentation on signed distance functions: 21 | 22 | - [3D Signed Distance Functions](https://iquilezles.org/www/articles/distfunctions/distfunctions.htm) 23 | - [2D Signed Distance Functions](https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm) 24 | 25 | ## Example 26 | 27 | 28 | 29 | Here is a complete example that generates the model shown. This is the 30 | canonical [Constructive Solid Geometry](https://en.wikipedia.org/wiki/Constructive_solid_geometry) 31 | example. Note the use of operators for union, intersection, and difference. 32 | 33 | ```python 34 | from sdf import * 35 | 36 | f = sphere(1) & box(1.5) 37 | 38 | c = cylinder(0.5) 39 | f -= c.orient(X) | c.orient(Y) | c.orient(Z) 40 | 41 | f.save('out.stl') 42 | ``` 43 | 44 | Yes, that's really the entire code! You can 3D print that model or use it 45 | in a 3D application. 46 | 47 | ## More Examples 48 | 49 | Have a cool example? Submit a PR! 50 | 51 | | [gearlike.py](examples/gearlike.py) | [knurling.py](examples/knurling.py) | [blobby.py](examples/blobby.py) | [weave.py](examples/weave.py) | 52 | | --- | --- | --- | --- | 53 | | ![gearlike](docs/images/gearlike.png) | ![knurling](docs/images/knurling.png) | ![blobby](docs/images/blobby.png) | ![weave](docs/images/weave.png) | 54 | | ![gearlike](docs/images/gearlike.jpg) | ![knurling](docs/images/knurling.jpg) | ![blobby](docs/images/blobby.jpg) | ![weave](docs/images/weave.jpg) | 55 | 56 | ## Requirements 57 | 58 | Note that the dependencies will be automatically installed by setup.py when 59 | following the directions below. 60 | 61 | - Python 3 62 | - numpy 63 | - Pillow 64 | - scikit-image 65 | - scipy 66 | 67 | ## Installation 68 | 69 | Use the commands below to clone the repository and install the `sdf` library 70 | in a Python virtualenv. 71 | 72 | ```bash 73 | git clone https://github.com/fogleman/sdf.git 74 | cd sdf 75 | virtualenv env 76 | . env/bin/activate 77 | pip install -e . 78 | ``` 79 | 80 | Confirm that it works: 81 | 82 | ```bash 83 | python examples/example.py # should generate a file named out.stl 84 | ``` 85 | 86 | You can skip the installation if you always run scripts that import `sdf` 87 | from the root folder. 88 | 89 | ## Viewing the Mesh 90 | 91 | 92 | 93 | Find and install a 3D mesh viewer for your platform, such as [MeshLab](https://www.meshlab.net/). 94 | 95 | I have developed and use my own cross-platform mesh viewer called [meshview](https://github.com/fogleman/meshview) (see screenshot). 96 | Installation is easy if you have [Go](https://golang.org/) and [glfw](https://www.glfw.org/) installed: 97 | 98 | ```bash 99 | $ brew install go glfw # on macOS with homebrew 100 | $ go get -u github.com/fogleman/meshview/cmd/meshview 101 | ``` 102 | 103 | Then you can view any mesh from the command line with: 104 | 105 | ```bash 106 | $ meshview your-mesh.stl 107 | ``` 108 | 109 | See the meshview [README](https://github.com/fogleman/meshview) for more complete installation instructions. 110 | 111 | On macOS you can just use the built-in Quick Look (press spacebar after selecting the STL file in Finder) in a pinch. 112 | 113 | # API 114 | 115 | In all of the below examples, `f` is any 3D SDF, such as: 116 | 117 | ```python 118 | f = sphere() 119 | ``` 120 | 121 | ## Bounds 122 | 123 | The bounding box of the SDF is automatically estimated. Inexact SDFs such as 124 | non-uniform scaling may cause issues with this process. In that case you can 125 | specify the bounds to sample manually: 126 | 127 | ```python 128 | f.save('out.stl', bounds=((-1, -1, -1), (1, 1, 1))) 129 | ``` 130 | 131 | ## Resolution 132 | 133 | The resolution of the mesh is also computed automatically. There are two ways 134 | to specify the resolution. You can set the resolution directly with `step`: 135 | 136 | ```python 137 | f.save('out.stl', step=0.01) 138 | f.save('out.stl', step=(0.01, 0.02, 0.03)) # non-uniform resolution 139 | ``` 140 | 141 | Or you can specify approximately how many points to sample: 142 | 143 | ```python 144 | f.save('out.stl', samples=2**24) # sample about 16M points 145 | ``` 146 | 147 | By default, `samples=2**22` is used. 148 | 149 | *Tip*: Use the default resolution while developing your SDF. Then when you're done, 150 | crank up the resolution for your final output. 151 | 152 | ## Batches 153 | 154 | The SDF is sampled in batches. By default the batches have `32**3 = 32768` 155 | points each. This batch size can be overridden: 156 | 157 | ```python 158 | f.save('out.stl', batch_size=64) # instead of 32 159 | ``` 160 | 161 | The code attempts to skip any batches that are far away from the surface of 162 | the mesh. Inexact SDFs such as non-uniform scaling may cause issues with this 163 | process, resulting in holes in the output mesh (where batches were skipped when 164 | they shouldn't have been). To avoid this, you can disable sparse sampling: 165 | 166 | ```python 167 | f.save('out.stl', sparse=False) # force all batches to be completely sampled 168 | ``` 169 | 170 | ## Worker Threads 171 | 172 | The SDF is sampled in batches using worker threads. By default, 173 | `multiprocessing.cpu_count()` worker threads are used. This can be overridden: 174 | 175 | ```python 176 | f.save('out.stl', workers=1) # only use one worker thread 177 | ``` 178 | 179 | ## Without Saving 180 | 181 | You can of course generate a mesh without writing it to an STL file: 182 | 183 | ```python 184 | points = f.generate() # takes the same optional arguments as `save` 185 | print(len(points)) # print number of points (3x the number of triangles) 186 | print(points[:3]) # print the vertices of the first triangle 187 | ``` 188 | 189 | If you want to save an STL after `generate`, just use: 190 | 191 | ```python 192 | write_binary_stl(path, points) 193 | ``` 194 | 195 | ## Visualizing the SDF 196 | 197 | 198 | 199 | You can plot a visualization of a 2D slice of the SDF using matplotlib. 200 | This can be useful for debugging purposes. 201 | 202 | ```python 203 | f.show_slice(z=0) 204 | f.show_slice(z=0, abs=True) # show abs(f) 205 | ``` 206 | 207 | You can specify a slice plane at any X, Y, or Z coordinate. You can 208 | also specify the bounds to plot. 209 | 210 | Note that `matplotlib` is only imported if this function is called, so it 211 | isn't strictly required as a dependency. 212 | 213 |
214 | 215 | ## How it Works 216 | 217 | The code simply uses the [Marching Cubes](https://en.wikipedia.org/wiki/Marching_cubes) 218 | algorithm to generate a mesh from the [Signed Distance Function](https://en.wikipedia.org/wiki/Signed_distance_function). 219 | 220 | This would normally be abysmally slow in Python. However, numpy is used to 221 | evaluate the SDF on entire batches of points simultaneously. Furthermore, 222 | multiple threads are used to process batches in parallel. The result is 223 | surprisingly fast (for marching cubes). Meshes of adequate detail can 224 | still be quite large in terms of number of triangles. 225 | 226 | The core "engine" of the `sdf` library is very small and can be found in 227 | [mesh.py](https://github.com/fogleman/sdf/blob/main/sdf/mesh.py). 228 | 229 | In short, there is nothing algorithmically revolutionary here. The goal is 230 | to provide a simple, fun, and easy-to-use API for generating 3D models in our 231 | favorite language Python. 232 | 233 | ## Files 234 | 235 | - [sdf/d2.py](https://github.com/fogleman/sdf/blob/main/sdf/d2.py): 2D signed distance functions 236 | - [sdf/d3.py](https://github.com/fogleman/sdf/blob/main/sdf/d3.py): 3D signed distance functions 237 | - [sdf/dn.py](https://github.com/fogleman/sdf/blob/main/sdf/dn.py): Dimension-agnostic signed distance functions 238 | - [sdf/ease.py](https://github.com/fogleman/sdf/blob/main/sdf/ease.py): [Easing functions](https://easings.net/) that operate on numpy arrays. Some SDFs take an easing function as a parameter. 239 | - [sdf/mesh.py](https://github.com/fogleman/sdf/blob/main/sdf/mesh.py): The core mesh-generation engine. Also includes code for estimating the bounding box of an SDF and for plotting a 2D slice of an SDF with matplotlib. 240 | - [sdf/progress.py](https://github.com/fogleman/sdf/blob/main/sdf/progress.py): A console progress bar. 241 | - [sdf/stl.py](https://github.com/fogleman/sdf/blob/main/sdf/stl.py): Code for writing a binary [STL file](https://en.wikipedia.org/wiki/STL_(file_format)). 242 | - [sdf/text.py](https://github.com/fogleman/sdf/blob/main/sdf/text.py): Generate 2D SDFs for text (which can then be extruded) 243 | - [sdf/util.py](https://github.com/fogleman/sdf/blob/main/sdf/util.py): Utility constants and functions. 244 | 245 | ## SDF Implementation 246 | 247 | It is reasonable to write your own SDFs beyond those provided by the 248 | built-in library. Browse the SDF implementations to understand how they are 249 | implemented. Here are some simple examples: 250 | 251 | ```python 252 | @sdf3 253 | def sphere(radius=1, center=ORIGIN): 254 | def f(p): 255 | return np.linalg.norm(p - center, axis=1) - radius 256 | return f 257 | ``` 258 | 259 | An SDF is simply a function that takes a numpy array of points with shape `(N, 3)` 260 | for 3D SDFs or shape `(N, 2)` for 2D SDFs and returns the signed distance for each 261 | of those points as an array of shape `(N, 1)`. They are wrapped with the 262 | `@sdf3` decorator (or `@sdf2` for 2D SDFs) which make boolean operators work, 263 | add the `save` method, add the operators like `translate`, etc. 264 | 265 | ```python 266 | @op3 267 | def translate(other, offset): 268 | def f(p): 269 | return other(p - offset) 270 | return f 271 | ``` 272 | 273 | An SDF that operates on another SDF (like the above `translate`) should use 274 | the `@op3` decorator instead. This will register the function such that SDFs 275 | can be chained together like: 276 | 277 | ```python 278 | f = sphere(1).translate((1, 2, 3)) 279 | ``` 280 | 281 | Instead of what would otherwise be required: 282 | 283 | ```python 284 | f = translate(sphere(1), (1, 2, 3)) 285 | ``` 286 | 287 | ## Remember, it's Python! 288 | 289 | 290 | 291 | Remember, this is Python, so it's fully programmable. You can and should split up your 292 | model into parameterized sub-components, for example. You can use for loops and 293 | conditionals wherever applicable. The sky is the limit! 294 | 295 | See the [customizable box example](examples/customizable_box.py) for some starting ideas. 296 | 297 |
298 | 299 | # Function Reference 300 | 301 | ## 3D Primitives 302 | 303 | ### sphere 304 | 305 | 306 | 307 | `sphere(radius=1, center=ORIGIN)` 308 | 309 | ```python 310 | f = sphere() # unit sphere 311 | f = sphere(2) # specify radius 312 | f = sphere(1, (1, 2, 3)) # translated sphere 313 | ``` 314 | 315 | ### box 316 | 317 | 318 | 319 | `box(size=1, center=ORIGIN, a=None, b=None)` 320 | 321 | ```python 322 | f = box(1) # all side lengths = 1 323 | f = box((1, 2, 3)) # different side lengths 324 | f = box(a=(-1, -1, -1), b=(3, 4, 5)) # specified by bounds 325 | ``` 326 | 327 | ### rounded_box 328 | 329 | 330 | 331 | `rounded_box(size, radius)` 332 | 333 | ```python 334 | f = rounded_box((1, 2, 3), 0.25) 335 | ``` 336 | 337 | ### wireframe_box 338 | 339 | 340 | `wireframe_box(size, thickness)` 341 | 342 | ```python 343 | f = wireframe_box((1, 2, 3), 0.05) 344 | ``` 345 | 346 | ### torus 347 | 348 | 349 | `torus(r1, r2)` 350 | 351 | ```python 352 | f = torus(1, 0.25) 353 | ``` 354 | 355 | ### capsule 356 | 357 | 358 | `capsule(a, b, radius)` 359 | 360 | ```python 361 | f = capsule(-Z, Z, 0.5) 362 | ``` 363 | 364 | ### capped_cylinder 365 | 366 | 367 | `capped_cylinder(a, b, radius)` 368 | 369 | ```python 370 | f = capped_cylinder(-Z, Z, 0.5) 371 | ``` 372 | 373 | ### rounded_cylinder 374 | 375 | 376 | `rounded_cylinder(ra, rb, h)` 377 | 378 | ```python 379 | f = rounded_cylinder(0.5, 0.1, 2) 380 | ``` 381 | 382 | ### capped_cone 383 | 384 | 385 | 386 | `capped_cone(a, b, ra, rb)` 387 | 388 | ```python 389 | f = capped_cone(-Z, Z, 1, 0.5) 390 | ``` 391 | 392 | ### rounded_cone 393 | 394 | 395 | 396 | `rounded_cone(r1, r2, h)` 397 | 398 | ```python 399 | f = rounded_cone(0.75, 0.25, 2) 400 | ``` 401 | 402 | ### ellipsoid 403 | 404 | 405 | 406 | `ellipsoid(size)` 407 | 408 | ```python 409 | f = ellipsoid((1, 2, 3)) 410 | ``` 411 | 412 | ### pyramid 413 | 414 | 415 | 416 | `pyramid(h)` 417 | 418 | ```python 419 | f = pyramid(1) 420 | ``` 421 | 422 | ## Platonic Solids 423 | 424 | ### tetrahedron 425 | 426 | 427 | 428 | `tetrahedron(r)` 429 | 430 | ```python 431 | f = tetrahedron(1) 432 | ``` 433 | 434 | ### octahedron 435 | 436 | 437 | 438 | `octahedron(r)` 439 | 440 | ```python 441 | f = octahedron(1) 442 | ``` 443 | 444 | ### dodecahedron 445 | 446 | 447 | 448 | `dodecahedron(r)` 449 | 450 | ```python 451 | f = dodecahedron(1) 452 | ``` 453 | 454 | ### icosahedron 455 | 456 | 457 | 458 | `icosahedron(r)` 459 | 460 | ```python 461 | f = icosahedron(1) 462 | ``` 463 | 464 | ## Infinite 3D Primitives 465 | 466 | The following SDFs extend to infinity in some or all axes. 467 | They can only effectively be used in combination with other shapes, as shown in the examples below. 468 | 469 | ### plane 470 | 471 | 472 | 473 | `plane(normal=UP, point=ORIGIN)` 474 | 475 | `plane` is an infinite plane, with one side being positive (outside) and one side being negative (inside). 476 | 477 | ```python 478 | f = sphere() & plane() 479 | ``` 480 | 481 | ### slab 482 | 483 | 484 | 485 | `slab(x0=None, y0=None, z0=None, x1=None, y1=None, z1=None, k=None)` 486 | 487 | `slab` is useful for cutting a shape on one or more axis-aligned planes. 488 | 489 | ```python 490 | f = sphere() & slab(z0=-0.5, z1=0.5, x0=0) 491 | ``` 492 | 493 | ### cylinder 494 | 495 | 496 | 497 | `cylinder(radius)` 498 | 499 | `cylinder` is an infinite cylinder along the Z axis. 500 | 501 | ```python 502 | f = sphere() - cylinder(0.5) 503 | ``` 504 | 505 | ## Text 506 | 507 | Yes, even text is supported! 508 | 509 | ![Text](docs/images/text-large.png) 510 | 511 | 512 | 513 | `text(name, text, width=None, height=None, texture_point_size=512)` 514 | 515 | ```python 516 | FONT = 'Arial' 517 | TEXT = 'Hello, world!' 518 | 519 | w, h = measure_text(FONT, TEXT) 520 | 521 | f = rounded_box((w + 1, h + 1, 0.2), 0.1) 522 | f -= text(FONT, TEXT).extrude(1) 523 | ``` 524 | 525 | ## Positioning 526 | 527 | ### translate 528 | 529 | 530 | 531 | `translate(other, offset)` 532 | 533 | ```python 534 | f = sphere().translate((0, 0, 2)) 535 | ``` 536 | 537 | ### scale 538 | 539 | 540 | 541 | `scale(other, factor)` 542 | 543 | Note that non-uniform scaling is an inexact SDF. 544 | 545 | ```python 546 | f = sphere().scale(2) 547 | f = sphere().scale((1, 2, 3)) # non-uniform scaling 548 | ``` 549 | 550 | ### rotate 551 | 552 | 553 | 554 | `rotate(other, angle, vector=Z)` 555 | 556 | ```python 557 | f = capped_cylinder(-Z, Z, 0.5).rotate(pi / 4, X) 558 | ``` 559 | 560 | ### orient 561 | 562 | 563 | 564 | `orient(other, axis)` 565 | 566 | `orient` rotates the shape such that whatever was pointing in the +Z direction 567 | is now pointing in the specified direction. 568 | 569 | ```python 570 | c = capped_cylinder(-Z, Z, 0.25) 571 | f = c.orient(X) | c.orient(Y) | c.orient(Z) 572 | ``` 573 | 574 | ## Boolean Operations 575 | 576 | The following primitives `a` and `b` are used in all of the following 577 | boolean operations. 578 | 579 | ```python 580 | a = box((3, 3, 0.5)) 581 | b = sphere() 582 | ``` 583 | 584 | The named versions (`union`, `difference`, `intersection`) can all take 585 | one or more SDFs as input. They all take an optional `k` parameter to define the amount 586 | of smoothing to apply. When using operators (`|`, `-`, `&`) the smoothing can 587 | still be applied via the `.k(...)` function. 588 | 589 | ### union 590 | 591 | 592 | 593 | ```python 594 | f = a | b 595 | f = union(a, b) # equivalent 596 | ``` 597 | 598 |
599 | 600 | ### difference 601 | 602 | 603 | 604 | ```python 605 | f = a - b 606 | f = difference(a, b) # equivalent 607 | ``` 608 | 609 |
610 | 611 | ### intersection 612 | 613 | 614 | 615 | ```python 616 | f = a & b 617 | f = intersection(a, b) # equivalent 618 | ``` 619 | 620 |
621 | 622 | ### smooth_union 623 | 624 | 625 | 626 | ```python 627 | f = a | b.k(0.25) 628 | f = union(a, b, k=0.25) # equivalent 629 | ``` 630 | 631 |
632 | 633 | ### smooth_difference 634 | 635 | 636 | 637 | ```python 638 | f = a - b.k(0.25) 639 | f = difference(a, b, k=0.25) # equivalent 640 | ``` 641 | 642 |
643 | 644 | ### smooth_intersection 645 | 646 | 647 | 648 | ```python 649 | f = a & b.k(0.25) 650 | f = intersection(a, b, k=0.25) # equivalent 651 | ``` 652 | 653 |
654 | 655 | ## Repetition 656 | 657 | ### repeat 658 | 659 | 660 | 661 | `repeat(other, spacing, count=None, padding=0)` 662 | 663 | `repeat` can repeat the underlying SDF infinitely or a finite number of times. 664 | If finite, the number of repetitions must be odd, because the count specifies 665 | the number of copies to make on each side of the origin. If the repeated 666 | elements overlap or come close together, you made need to specify a `padding` 667 | greater than zero to compute a correct SDF. 668 | 669 | ```python 670 | f = sphere().repeat(3, (1, 1, 0)) 671 | ``` 672 | 673 | ### circular_array 674 | 675 | 676 | 677 | `circular_array(other, count, offset)` 678 | 679 | `circular_array` makes `count` copies of the underlying SDF, arranged in a 680 | circle around the Z axis. `offset` specifies how far to translate the shape 681 | in X before arraying it. The underlying SDF is only evaluated twice (instead 682 | of `count` times), so this is more performant than instantiating `count` copies 683 | of a shape. 684 | 685 | ```python 686 | f = capped_cylinder(-Z, Z, 0.5).circular_array(8, 4) 687 | ``` 688 | 689 | ## Miscellaneous 690 | 691 | ### blend 692 | 693 | 694 | 695 | `blend(a, *bs, k=0.5)` 696 | 697 | ```python 698 | f = sphere().blend(box()) 699 | ``` 700 | 701 | ### dilate 702 | 703 | 704 | 705 | `dilate(other, r)` 706 | 707 | ```python 708 | f = example.dilate(0.1) 709 | ``` 710 | 711 | ### erode 712 | 713 | 714 | 715 | `erode(other, r)` 716 | 717 | ```python 718 | f = example.erode(0.1) 719 | ``` 720 | 721 | ### shell 722 | 723 | 724 | 725 | `shell(other, thickness)` 726 | 727 | ```python 728 | f = sphere().shell(0.05) & plane(-Z) 729 | ``` 730 | 731 | ### elongate 732 | 733 | 734 | 735 | `elongate(other, size)` 736 | 737 | ```python 738 | f = example.elongate((0.25, 0.5, 0.75)) 739 | ``` 740 | 741 | ### twist 742 | 743 | 744 | 745 | `twist(other, k)` 746 | 747 | ```python 748 | f = box().twist(pi / 2) 749 | ``` 750 | 751 | ### bend 752 | 753 | 754 | 755 | `bend(other, k)` 756 | 757 | ```python 758 | f = box().bend(1) 759 | ``` 760 | 761 | ### bend_linear 762 | 763 | 764 | 765 | `bend_linear(other, p0, p1, v, e=ease.linear)` 766 | 767 | ```python 768 | f = capsule(-Z * 2, Z * 2, 0.25).bend_linear(-Z, Z, X, ease.in_out_quad) 769 | ``` 770 | 771 | ### bend_radial 772 | 773 | 774 | 775 | `bend_radial(other, r0, r1, dz, e=ease.linear)` 776 | 777 | ```python 778 | f = box((5, 5, 0.25)).bend_radial(1, 2, -1, ease.in_out_quad) 779 | ``` 780 | 781 | ### transition_linear 782 | 783 | 784 | 785 | `transition_linear(f0, f1, p0=-Z, p1=Z, e=ease.linear)` 786 | 787 | ```python 788 | f = box().transition_linear(sphere(), e=ease.in_out_quad) 789 | ``` 790 | 791 | ### transition_radial 792 | 793 | 794 | 795 | `transition_radial(f0, f1, r0=0, r1=1, e=ease.linear)` 796 | 797 | ```python 798 | f = box().transition_radial(sphere(), e=ease.in_out_quad) 799 | ``` 800 | 801 | ### wrap_around 802 | 803 | 804 | 805 | `wrap_around(other, x0, x1, r=None, e=ease.linear)` 806 | 807 | ```python 808 | FONT = 'Arial' 809 | TEXT = ' wrap_around ' * 3 810 | w, h = measure_text(FONT, TEXT) 811 | f = text(FONT, TEXT).extrude(0.1).orient(Y).wrap_around(-w / 2, w / 2) 812 | ``` 813 | 814 | ## 2D to 3D Operations 815 | 816 | ### extrude 817 | 818 | 819 | 820 | `extrude(other, h)` 821 | 822 | ```python 823 | f = hexagon(1).extrude(1) 824 | ``` 825 | 826 | ### extrude_to 827 | 828 | 829 | 830 | `extrude_to(a, b, h, e=ease.linear)` 831 | 832 | ```python 833 | f = rectangle(2).extrude_to(circle(1), 2, ease.in_out_quad) 834 | ``` 835 | 836 | ### revolve 837 | 838 | 839 | 840 | `revolve(other, offset=0)` 841 | 842 | ```python 843 | f = hexagon(1).revolve(3) 844 | ``` 845 | 846 | ## 3D to 2D Operations 847 | 848 | ### slice 849 | 850 | 851 | 852 | `slice(other)` 853 | 854 | ```python 855 | f = example.translate((0, 0, 0.55)).slice().extrude(0.1) 856 | ``` 857 | 858 | ## 2D Primitives 859 | 860 | ### circle 861 | ### line 862 | ### rectangle 863 | ### rounded_rectangle 864 | ### equilateral_triangle 865 | ### hexagon 866 | ### rounded_x 867 | ### polygon 868 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /models 2 | /render 3 | 4 | -------------------------------------------------------------------------------- /docs/images/bend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/bend.png -------------------------------------------------------------------------------- /docs/images/bend_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/bend_linear.png -------------------------------------------------------------------------------- /docs/images/bend_radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/bend_radial.png -------------------------------------------------------------------------------- /docs/images/blend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/blend.png -------------------------------------------------------------------------------- /docs/images/blobby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/blobby.jpg -------------------------------------------------------------------------------- /docs/images/blobby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/blobby.png -------------------------------------------------------------------------------- /docs/images/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/box.png -------------------------------------------------------------------------------- /docs/images/box2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/box2.png -------------------------------------------------------------------------------- /docs/images/capped_cone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/capped_cone.png -------------------------------------------------------------------------------- /docs/images/capped_cylinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/capped_cylinder.png -------------------------------------------------------------------------------- /docs/images/capsule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/capsule.png -------------------------------------------------------------------------------- /docs/images/circular_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/circular_array.png -------------------------------------------------------------------------------- /docs/images/customizable_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/customizable_box.png -------------------------------------------------------------------------------- /docs/images/customizable_lid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/customizable_lid.png -------------------------------------------------------------------------------- /docs/images/cylinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/cylinder.png -------------------------------------------------------------------------------- /docs/images/difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/difference.png -------------------------------------------------------------------------------- /docs/images/dilate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/dilate.png -------------------------------------------------------------------------------- /docs/images/dodecahedron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/dodecahedron.png -------------------------------------------------------------------------------- /docs/images/ellipsoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/ellipsoid.png -------------------------------------------------------------------------------- /docs/images/elongate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/elongate.png -------------------------------------------------------------------------------- /docs/images/erode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/erode.png -------------------------------------------------------------------------------- /docs/images/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/example.jpg -------------------------------------------------------------------------------- /docs/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/example.png -------------------------------------------------------------------------------- /docs/images/extrude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/extrude.png -------------------------------------------------------------------------------- /docs/images/extrude_to.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/extrude_to.png -------------------------------------------------------------------------------- /docs/images/gearlike.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/gearlike.jpg -------------------------------------------------------------------------------- /docs/images/gearlike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/gearlike.png -------------------------------------------------------------------------------- /docs/images/icosahedron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/icosahedron.png -------------------------------------------------------------------------------- /docs/images/intersection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/intersection.png -------------------------------------------------------------------------------- /docs/images/knurling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/knurling.jpg -------------------------------------------------------------------------------- /docs/images/knurling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/knurling.png -------------------------------------------------------------------------------- /docs/images/meshview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/meshview.png -------------------------------------------------------------------------------- /docs/images/octahedron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/octahedron.png -------------------------------------------------------------------------------- /docs/images/orient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/orient.png -------------------------------------------------------------------------------- /docs/images/plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/plane.png -------------------------------------------------------------------------------- /docs/images/pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/pyramid.png -------------------------------------------------------------------------------- /docs/images/repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/repeat.png -------------------------------------------------------------------------------- /docs/images/revolve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/revolve.png -------------------------------------------------------------------------------- /docs/images/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/rotate.png -------------------------------------------------------------------------------- /docs/images/rounded_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/rounded_box.png -------------------------------------------------------------------------------- /docs/images/rounded_cone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/rounded_cone.png -------------------------------------------------------------------------------- /docs/images/rounded_cylinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/rounded_cylinder.png -------------------------------------------------------------------------------- /docs/images/scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/scale.png -------------------------------------------------------------------------------- /docs/images/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/shell.png -------------------------------------------------------------------------------- /docs/images/show_slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/show_slice.png -------------------------------------------------------------------------------- /docs/images/slab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/slab.png -------------------------------------------------------------------------------- /docs/images/slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/slice.png -------------------------------------------------------------------------------- /docs/images/smooth_difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/smooth_difference.png -------------------------------------------------------------------------------- /docs/images/smooth_intersection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/smooth_intersection.png -------------------------------------------------------------------------------- /docs/images/smooth_union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/smooth_union.png -------------------------------------------------------------------------------- /docs/images/sphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/sphere.png -------------------------------------------------------------------------------- /docs/images/tetrahedron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/tetrahedron.png -------------------------------------------------------------------------------- /docs/images/text-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/text-large.png -------------------------------------------------------------------------------- /docs/images/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/text.png -------------------------------------------------------------------------------- /docs/images/torus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/torus.png -------------------------------------------------------------------------------- /docs/images/transition_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/transition_linear.png -------------------------------------------------------------------------------- /docs/images/transition_radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/transition_radial.png -------------------------------------------------------------------------------- /docs/images/translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/translate.png -------------------------------------------------------------------------------- /docs/images/twist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/twist.png -------------------------------------------------------------------------------- /docs/images/union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/union.png -------------------------------------------------------------------------------- /docs/images/weave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/weave.jpg -------------------------------------------------------------------------------- /docs/images/weave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/weave.png -------------------------------------------------------------------------------- /docs/images/wireframe_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/wireframe_box.png -------------------------------------------------------------------------------- /docs/images/wrap_around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/sdf-torch/fde71b2a85734e31c92f830ddd0dd58605c23dd1/docs/images/wrap_around.png -------------------------------------------------------------------------------- /docs/render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | . "github.com/fogleman/fauxgl" 10 | "github.com/nfnt/resize" 11 | ) 12 | 13 | const ( 14 | aa = 4 15 | width = 1024 16 | height = 1024 17 | near = 1 18 | far = 10 19 | ) 20 | 21 | var ( 22 | eye = V(3, 3, 3) 23 | center = V(0, 0, 0) 24 | up = V(0, 0, 1) 25 | 26 | axisLight = V(1, 1, 1) 27 | modelLight = V(0.75, 0.25, 1) 28 | 29 | xColor = HexColor("BF1506") 30 | yColor = HexColor("5ABF56") 31 | zColor = HexColor("1B52BF") 32 | originColor = HexColor("333333") 33 | modelColor = HexColor("2185C5") 34 | 35 | background = Transparent 36 | ) 37 | 38 | func main() { 39 | // parse command line arguments 40 | args := os.Args[1:] 41 | if len(args) != 2 { 42 | log.Fatal("Usage: go run render.go input.stl output.png") 43 | } 44 | 45 | // load mesh 46 | mesh, err := LoadMesh(os.Args[1]) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | // scale mesh to fit in a bi-unit cube centered at the origin 52 | // but do not translate it 53 | box := mesh.BoundingBox() 54 | h := box.Max.Abs().Max(box.Min.Abs()) 55 | s := V(1, 1, 1).Div(h).MinComponent() 56 | mesh.Transform(Scale(V(s, s, s))) 57 | 58 | // create rendering context 59 | context := NewContext(width*aa, height*aa) 60 | context.ClearColorBufferWith(background) 61 | 62 | // create transformation matrix 63 | matrix := LookAt(eye, center, up) 64 | matrix = matrix.Orthographic(-2, 2, -2, 2, near, far) 65 | 66 | // render axes and origin 67 | { 68 | shader := NewPhongShader(matrix, axisLight.Normalize(), eye) 69 | shader.AmbientColor = Gray(0.4) 70 | shader.DiffuseColor = Gray(0.7) 71 | shader.SpecularColor = Gray(0) 72 | context.Shader = shader 73 | 74 | axes := []Vector{ 75 | V(1, 0, 0), 76 | V(0, 1, 0), 77 | V(0, 0, 1), 78 | } 79 | 80 | colors := []Color{xColor, yColor, zColor} 81 | 82 | for i, axis := range axes { 83 | shader.ObjectColor = colors[i] 84 | 85 | c := NewCylinder(30, false) 86 | c.Transform(Scale(V(0.01, 0.01, 2))) 87 | c.Transform(Translate(V(0, 0, 1))) 88 | c.Transform(RotateTo(up, axis)) 89 | c.SmoothNormals() 90 | context.DrawMesh(c) 91 | 92 | c = NewCone(30, false) 93 | c.Transform(Scale(V(0.03, 0.03, 0.1))) 94 | c.Transform(Translate(V(0, 0, 2))) 95 | c.Transform(RotateTo(up, axis)) 96 | c.SmoothNormals() 97 | context.DrawMesh(c) 98 | } 99 | 100 | shader.ObjectColor = originColor 101 | c := NewSphere(2) 102 | c.Transform(Scale(V(0.025, 0.025, 0.025))) 103 | c.SmoothNormals() 104 | context.DrawMesh(c) 105 | } 106 | 107 | // render mesh 108 | { 109 | shader := NewPhongShader(matrix, modelLight.Normalize(), eye) 110 | shader.ObjectColor = modelColor 111 | shader.AmbientColor = Gray(0.3) 112 | shader.DiffuseColor = Gray(0.9) 113 | shader.SpecularColor = Gray(0.2) 114 | shader.SpecularPower = 10 115 | context.Shader = shader 116 | start := time.Now() 117 | info := context.DrawMesh(mesh) 118 | fmt.Println(info) 119 | fmt.Println(time.Since(start)) 120 | } 121 | 122 | // save image 123 | image := context.Image() 124 | image = resize.Resize(width, height, image, resize.Bilinear) 125 | SavePNG(os.Args[2], image) 126 | } 127 | -------------------------------------------------------------------------------- /docs/render.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | import os 3 | 4 | 5 | def generate(f, name, samples=2 ** 26, **kwargs): 6 | os.makedirs("models", exist_ok=True) 7 | os.makedirs("images", exist_ok=True) 8 | stl_path = "models/%s.stl" % name 9 | png_path = "images/%s.png" % name 10 | if os.path.exists(png_path): 11 | return 12 | render_cmd = "./render %s %s" % (stl_path, png_path) 13 | f.save(stl_path, samples=samples, **kwargs) 14 | os.system(render_cmd) 15 | 16 | 17 | # example 18 | f = sphere(1) & box(1.5) 19 | c = cylinder(0.5) 20 | f -= c.orient(X) | c.orient(Y) | c.orient(Z) 21 | example = f 22 | generate(f, "example") 23 | 24 | # sphere(radius=1, center=ORIGIN) 25 | f = sphere(1) 26 | generate(f, "sphere") 27 | 28 | # box(size=1, center=ORIGIN, a=None, b=None) 29 | f = box(1) 30 | generate(f, "box") 31 | 32 | f = box((1, 2, 3)) 33 | generate(f, "box2") 34 | 35 | # rounded_box(size, radius) 36 | f = rounded_box((1, 2, 3), 0.25) 37 | generate(f, "rounded_box") 38 | 39 | # wireframe_box(size, thickness) 40 | f = wireframe_box((1, 2, 3), 0.05) 41 | generate(f, "wireframe_box") 42 | 43 | # torus(r1, r2) 44 | f = torus(1, 0.25) 45 | generate(f, "torus") 46 | 47 | # capsule(a, b, radius) 48 | f = capsule(-Z, Z, 0.5) 49 | generate(f, "capsule") 50 | 51 | # capped_cylinder(a, b, radius) 52 | f = capped_cylinder(-Z, Z, 0.5) 53 | generate(f, "capped_cylinder") 54 | 55 | # rounded_cylinder(ra, rb, h) 56 | f = rounded_cylinder(0.5, 0.1, 2) 57 | generate(f, "rounded_cylinder") 58 | 59 | # capped_cone(a, b, ra, rb) 60 | f = capped_cone(-Z, Z, 1, 0.5) 61 | generate(f, "capped_cone") 62 | 63 | # rounded_cone(r1, r2, h) 64 | f = rounded_cone(0.75, 0.25, 2) 65 | generate(f, "rounded_cone") 66 | 67 | # ellipsoid(size) 68 | f = ellipsoid((1, 2, 3)) 69 | generate(f, "ellipsoid") 70 | 71 | # pyramid(h) 72 | f = pyramid(1) 73 | generate(f, "pyramid") 74 | 75 | # tetrahedron(r) 76 | f = tetrahedron(1) 77 | generate(f, "tetrahedron") 78 | 79 | # octahedron(r) 80 | f = octahedron(1) 81 | generate(f, "octahedron") 82 | 83 | # dodecahedron(r) 84 | f = dodecahedron(1) 85 | generate(f, "dodecahedron") 86 | 87 | # icosahedron(r) 88 | f = icosahedron(1) 89 | generate(f, "icosahedron") 90 | 91 | # plane(normal=UP, point=ORIGIN) 92 | f = sphere() & plane() 93 | generate(f, "plane") 94 | 95 | # slab(x0=None, y0=None, z0=None, x1=None, y1=None, z1=None, k=None) 96 | f = sphere() & slab(z0=-0.5, z1=0.5, x0=0) 97 | generate(f, "slab") 98 | 99 | # cylinder(radius) 100 | f = sphere() - cylinder(0.5) 101 | generate(f, "cylinder") 102 | 103 | # translate(other, offset) 104 | f = sphere().translate((0, 0, 2)) 105 | generate(f, "translate") 106 | 107 | # scale(other, factor) 108 | f = sphere().scale((1, 2, 3)) 109 | generate(f, "scale") 110 | 111 | # rotate(other, angle, vector=Z) 112 | # rotate_to(other, a, b) 113 | f = capped_cylinder(-Z, Z, 0.5).rotate(pi / 4, X) 114 | generate(f, "rotate") 115 | 116 | # orient(other, axis) 117 | c = capped_cylinder(-Z, Z, 0.25) 118 | f = c.orient(X) | c.orient(Y) | c.orient(Z) 119 | generate(f, "orient") 120 | 121 | # boolean operations 122 | 123 | a = box((3, 3, 0.5)) 124 | b = sphere() 125 | 126 | # union 127 | f = a | b 128 | generate(f, "union") 129 | 130 | # difference 131 | f = a - b 132 | generate(f, "difference") 133 | 134 | # intersection 135 | f = a & b 136 | generate(f, "intersection") 137 | 138 | # smooth union 139 | f = a | b.k(0.25) 140 | generate(f, "smooth_union") 141 | 142 | # smooth difference 143 | f = a - b.k(0.25) 144 | generate(f, "smooth_difference") 145 | 146 | # smooth intersection 147 | f = a & b.k(0.25) 148 | generate(f, "smooth_intersection") 149 | 150 | # repeat(other, spacing, count=None, padding=0) 151 | f = sphere().repeat(3, (1, 1, 0)) 152 | generate(f, "repeat") 153 | 154 | # circular_array(other, count, offset) 155 | f = capped_cylinder(-Z, Z, 0.5).circular_array(8, 4) 156 | generate(f, "circular_array") 157 | 158 | # blend(a, *bs, k=0.5) 159 | f = sphere().blend(box()) 160 | generate(f, "blend") 161 | 162 | # dilate(other, r) 163 | f = example.dilate(0.1) 164 | generate(f, "dilate") 165 | 166 | # erode(other, r) 167 | f = example.erode(0.1) 168 | generate(f, "erode") 169 | 170 | # shell(other, thickness) 171 | f = sphere().shell(0.05) & plane(-Z) 172 | generate(f, "shell") 173 | 174 | # elongate(other, size) 175 | f = example.elongate((0.25, 0.5, 0.75)) 176 | generate(f, "elongate") 177 | 178 | # twist(other, k) 179 | f = box().twist(pi / 2) 180 | generate(f, "twist") 181 | 182 | # bend(other, k) 183 | f = box().bend(1) 184 | generate(f, "bend") 185 | 186 | # bend_linear(other, p0, p1, v, e=ease.linear) 187 | f = capsule(-Z * 2, Z * 2, 0.25).bend_linear(-Z, Z, X, ease.in_out_quad) 188 | generate(f, "bend_linear") 189 | 190 | # bend_radial(other, r0, r1, dz, e=ease.linear) 191 | f = box((5, 5, 0.25)).bend_radial(1, 2, -1, ease.in_out_quad) 192 | generate(f, "bend_radial", sparse=False) 193 | 194 | # transition_linear(f0, f1, p0=-Z, p1=Z, e=ease.linear) 195 | f = box().transition_linear(sphere(), e=ease.in_out_quad) 196 | generate(f, "transition_linear") 197 | 198 | # transition_radial(f0, f1, r0=0, r1=1, e=ease.linear) 199 | f = box().transition_radial(sphere(), e=ease.in_out_quad) 200 | generate(f, "transition_radial") 201 | 202 | # extrude(other, h) 203 | f = hexagon(1).extrude(1) 204 | generate(f, "extrude") 205 | 206 | # extrude_to(a, b, h, e=ease.linear) 207 | f = rectangle(2).extrude_to(circle(1), 2, ease.in_out_quad) 208 | generate(f, "extrude_to") 209 | 210 | # revolve(other, offset=0) 211 | f = hexagon(1).revolve(3) 212 | generate(f, "revolve") 213 | 214 | # slice(other) 215 | f = example.translate((0, 0, 0.55)).slice().extrude(0.1) 216 | generate(f, "slice") 217 | 218 | # text(name, text, width=None, height=None, texture_point_size=512) 219 | f = rounded_box((7, 2, 0.2), 0.1) 220 | f -= text("Georgia", "Hello, World!").extrude(0.2).rotate(pi).translate(0.1 * Z) 221 | generate(f, "text") 222 | 223 | # wrap_around(other, x0, x1, r=None, e=ease.linear) 224 | FONT = "Arial" 225 | TEXT = " wrap_around " * 3 226 | w, h = measure_text(FONT, TEXT) 227 | f = text(FONT, TEXT).extrude(0.1).orient(Y).wrap_around(-w / 2, w / 2) 228 | generate(f, "wrap_around") 229 | -------------------------------------------------------------------------------- /examples/blobby.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | s = sphere(0.75) 4 | s = s.translate(Z * -3) | s.translate(Z * 3) 5 | s = s.union(capsule(Z * -3, Z * 3, 0.5), k=1) 6 | 7 | f = sphere(1.5).union(s.orient(X), s.orient(Y), s.orient(Z), k=1) 8 | 9 | f.save("blobby.stl", samples=2 ** 26) 10 | -------------------------------------------------------------------------------- /examples/customizable_box.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | WIDTH = 12 4 | HEIGHT = 6 5 | DEPTH = 2 6 | ROWS = 3 7 | COLS = 5 8 | WALL_THICKNESS = 0.25 9 | WALL_RADIUS = 0.5 10 | BOTTOM_RADIUS = 0.25 11 | TOP_FILLET = 0.125 12 | DIVIDER_THICKNESS = 0.2 13 | ROW_DIVIDER_DEPTH = 1.75 14 | COL_DIVIDER_DEPTH = 1.5 15 | DIVIDER_FILLET = 0.1 16 | LID_THICKNESS = 0.25 17 | LID_DEPTH = 0.75 18 | LID_RADIUS = 0.125 19 | SAMPLES = 2 ** 24 20 | 21 | 22 | def dividers(): 23 | col_spacing = WIDTH / COLS 24 | row_spacing = HEIGHT / ROWS 25 | c = rounded_box((DIVIDER_THICKNESS, 1e9, COL_DIVIDER_DEPTH), DIVIDER_FILLET) 26 | c = c.translate(Z * COL_DIVIDER_DEPTH / 2) 27 | c = c.repeat((col_spacing, 0, 0)) 28 | r = rounded_box((1e9, DIVIDER_THICKNESS, ROW_DIVIDER_DEPTH), DIVIDER_FILLET) 29 | r = r.translate(Z * ROW_DIVIDER_DEPTH / 2) 30 | r = r.repeat((0, row_spacing, 0)) 31 | if COLS % 2 != 0: 32 | c = c.translate((col_spacing / 2, 0, 0)) 33 | if ROWS % 2 != 0: 34 | r = r.translate((0, row_spacing / 2, 0)) 35 | return c | r 36 | 37 | 38 | def box(): 39 | d = dividers() 40 | p = WALL_THICKNESS 41 | f = rounded_box((WIDTH - p, HEIGHT - p, 1e9), WALL_RADIUS) 42 | f &= slab(z0=p / 2).k(BOTTOM_RADIUS) 43 | d &= f 44 | f = f.shell(WALL_THICKNESS) 45 | f &= slab(z1=DEPTH).k(TOP_FILLET) 46 | return f | d 47 | 48 | 49 | def lid(): 50 | p = WALL_THICKNESS 51 | f = rounded_box((WIDTH + p, HEIGHT + p, 1e9), WALL_RADIUS) 52 | f &= slab(z0=p / 2).k(LID_RADIUS) 53 | f = f.shell(LID_THICKNESS) 54 | f &= slab(z1=LID_DEPTH).k(TOP_FILLET) 55 | return f 56 | 57 | 58 | box().save("box.stl", samples=SAMPLES) 59 | lid().save("lid.stl", samples=SAMPLES) 60 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | f = sphere(1) & box(1.5) 4 | 5 | c = cylinder(0.5) 6 | f -= c.orient(X) | c.orient(Y) | c.orient(Z) 7 | 8 | f.save("out.stl") 9 | -------------------------------------------------------------------------------- /examples/gearlike.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | f = sphere(2) & slab(z0=-0.5, z1=0.5).k(0.1) 4 | f -= cylinder(1).k(0.1) 5 | f -= cylinder(0.25).circular_array(16, 2).k(0.1) 6 | 7 | f.save("gearlike.stl", samples=2 ** 26) 8 | -------------------------------------------------------------------------------- /examples/knurling.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | # main body 4 | f = rounded_cylinder(1, 0.1, 5) 5 | 6 | # knurling 7 | x = box((1, 1, 4)).rotate(pi / 4) 8 | x = x.circular_array(24, 1.6) 9 | x = x.twist(0.75) | x.twist(-0.75) 10 | f -= x.k(0.1) 11 | 12 | # central hole 13 | f -= cylinder(0.5).k(0.1) 14 | 15 | # vent holes 16 | c = cylinder(0.25).orient(X) 17 | f -= c.translate(Z * -2.5).k(0.1) 18 | f -= c.translate(Z * 2.5).k(0.1) 19 | 20 | f.save("knurling.stl", samples=2 ** 26) 21 | -------------------------------------------------------------------------------- /examples/pawn.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | 4 | def section(z0, z1, d0, d1, e=ease.linear): 5 | f = cylinder(d0 / 2).transition(cylinder(d1 / 2), Z * z0, Z * z1, e) 6 | return f & slab(z0=z0, z1=z1) 7 | 8 | 9 | f = section(0, 0.2, 1, 1.25) 10 | f |= section(0.2, 0.3, 1.25, 1).k(0.05) 11 | f |= rounded_cylinder(0.6, 0.1, 0.2).translate(Z * 0.4).k(0.05) 12 | f |= section(0.5, 1.75, 1, 0.25, ease.out_quad).k(0.01) 13 | f |= section(1.75, 1.85, 0.25, 0.5).k(0.01) 14 | f |= section(1.85, 1.90, 0.5, 0.25).k(0.05) 15 | f |= sphere(0.3).translate(Z * 2.15).k(0.05) 16 | 17 | f.save("pawn.stl", samples=2 ** 26) 18 | -------------------------------------------------------------------------------- /examples/text.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | FONT = "Arial" 4 | TEXT = "Hello, world!" 5 | 6 | w, h = measure_text(FONT, TEXT) 7 | 8 | f = rounded_box((w + 1, h + 1, 0.2), 0.1) 9 | f -= text(FONT, TEXT).extrude(1) 10 | 11 | f.save("text.stl") 12 | -------------------------------------------------------------------------------- /examples/weave.py: -------------------------------------------------------------------------------- 1 | from sdf import * 2 | 3 | f = rounded_box([3.2, 1, 0.25], 0.1).translate((1.5, 0, 0.0625)) 4 | f = f.bend_linear(X * 0.75, X * 2.25, Z * -0.1875, ease.in_out_quad) 5 | f = f.circular_array(3, 0) 6 | 7 | f = f.repeat((2.7, 5.4, 0), padding=1) 8 | f |= f.translate((2.7 / 2, 2.7, 0)) 9 | 10 | f &= cylinder(10) 11 | f |= (cylinder(12) - cylinder(10)) & slab(z0=-0.5, z1=0.5).k(0.25) 12 | 13 | f.save("weave.stl") 14 | -------------------------------------------------------------------------------- /sdf/__init__.py: -------------------------------------------------------------------------------- 1 | from . import d2, d3, ease 2 | 3 | from .util import * 4 | 5 | from .d2 import * 6 | 7 | from .d3 import * 8 | 9 | from .text import measure_text, text 10 | 11 | from .mesh import generate, save, sample_slice, show_slice 12 | 13 | from .stl import write_binary_stl 14 | -------------------------------------------------------------------------------- /sdf/d2.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import numpy as np 3 | import torch 4 | 5 | from . import dn, d3, ease, torch_util as tu 6 | 7 | # Constants 8 | 9 | ORIGIN = np.array((0, 0)) 10 | 11 | X = np.array((1, 0)) 12 | Y = np.array((0, 1)) 13 | 14 | UP = Y 15 | 16 | # SDF Class 17 | 18 | _ops = {} 19 | 20 | 21 | class SDF2: 22 | def __init__(self, f): 23 | self.f = f 24 | 25 | def __call__(self, p): 26 | return self.f(p).reshape((-1, 1)) 27 | 28 | def __getattr__(self, name): 29 | if name in _ops: 30 | f = _ops[name] 31 | return functools.partial(f, self) 32 | raise AttributeError 33 | 34 | def __or__(self, other): 35 | return union(self, other) 36 | 37 | def __and__(self, other): 38 | return intersection(self, other) 39 | 40 | def __sub__(self, other): 41 | return difference(self, other) 42 | 43 | def k(self, k=None): 44 | self._k = k 45 | return self 46 | 47 | 48 | def sdf2(f): 49 | def wrapper(*args, **kwargs): 50 | return SDF2(f(*args, **kwargs)) 51 | 52 | return wrapper 53 | 54 | 55 | def op2(f): 56 | def wrapper(*args, **kwargs): 57 | return SDF2(f(*args, **kwargs)) 58 | 59 | _ops[f.__name__] = wrapper 60 | return wrapper 61 | 62 | 63 | def op23(f): 64 | def wrapper(*args, **kwargs): 65 | return d3.SDF3(f(*args, **kwargs)) 66 | 67 | _ops[f.__name__] = wrapper 68 | return wrapper 69 | 70 | 71 | # Helpers 72 | 73 | 74 | def _length(a): 75 | return torch.linalg.norm(a, dim=1) 76 | 77 | 78 | def _normalize_np(a): 79 | return a / np.linalg.norm(a) 80 | 81 | 82 | def _dot(a, b): 83 | return (a * b).sum(1) 84 | 85 | 86 | _vec = tu.vec 87 | _min = tu.torch_min 88 | _max = tu.torch_max 89 | 90 | # Primitives 91 | 92 | 93 | @sdf2 94 | def circle(radius=1, center=ORIGIN): 95 | def f(p): 96 | return _length(p - tu.to_torch(p, center)) - radius 97 | 98 | return f 99 | 100 | 101 | @sdf2 102 | def line(normal=UP, point=ORIGIN): 103 | normal = _normalize_np(normal) 104 | 105 | def f(p): 106 | return torch.mv(tu.to_torch(p, point) - p, tu.to_torch(p, normal)) 107 | 108 | return f 109 | 110 | 111 | @sdf2 112 | def slab(x0=None, y0=None, x1=None, y1=None, k=None): 113 | fs = [] 114 | if x0 is not None: 115 | fs.append(line(X, (x0, 0))) 116 | if x1 is not None: 117 | fs.append(line(-X, (x1, 0))) 118 | if y0 is not None: 119 | fs.append(line(Y, (0, y0))) 120 | if y1 is not None: 121 | fs.append(line(-Y, (0, y1))) 122 | return intersection(*fs, k=k) 123 | 124 | 125 | @sdf2 126 | def rectangle(size=1, center=ORIGIN, a=None, b=None): 127 | if a is not None and b is not None: 128 | a = np.array(a) 129 | b = np.array(b) 130 | size = b - a 131 | center = a + size / 2 132 | return rectangle(size, center) 133 | size = np.array(size) 134 | 135 | def f(p): 136 | q = (p - tu.to_torch(p, center)).abs() - tu.to_torch(p, size / 2) 137 | return _length(_max(q, 0)) + _min(q.amax(1), 0) 138 | 139 | return f 140 | 141 | 142 | @sdf2 143 | def rounded_rectangle(size, radius, center=ORIGIN): 144 | try: 145 | r0, r1, r2, r3 = radius 146 | except TypeError: 147 | r0 = r1 = r2 = r3 = radius 148 | 149 | def f(p): 150 | x = p[:, 0] 151 | y = p[:, 1] 152 | r = torch.zeros_like(x) 153 | r[torch.logical_and(x > 0, y > 0)] = r0 154 | r[torch.logical_and(x > 0, y <= 0)] = r1 155 | r[torch.logical_and(x <= 0, y <= 0)] = r2 156 | r[torch.logical_and(x <= 0, y > 0)] = r3 157 | q = p.abs() - tu.to_torch(p, size / 2 + r) 158 | return ( 159 | _min(_max(q[:, 0], q[:, 1]), 0).reshape((-1, 1)) 160 | + _length(_max(q, 0)).reshape((-1, 1)) 161 | - r 162 | ) 163 | 164 | return f 165 | 166 | 167 | @sdf2 168 | def equilateral_triangle(): 169 | def f(p): 170 | k = 3 ** 0.5 171 | p = _vec(p[:, 0].abs() - 1, p[:, 1] + 1 / k) 172 | w = p[:, 0] + k * p[:, 1] > 0 173 | q = _vec(p[:, 0] - k * p[:, 1], -k * p[:, 0] - p[:, 1]) / 2 174 | p = torch.where(w.reshape((-1, 1)), q, p) 175 | p = _vec(p[:, 0] - p[:, 0].clamp(-2, 0), p[:, 1]) 176 | return -_length(p) * torch.sign(p[:, 1]) 177 | 178 | return f 179 | 180 | 181 | @sdf2 182 | def hexagon(r_np): 183 | def f(p): 184 | r = tu.to_torch(p, r_np) 185 | k = torch.tensor([3 ** 0.5 / -2, 0.5, np.tan(np.pi / 6)]).to(p) 186 | p = p.abs() 187 | p -= 2 * k[:2] * _min(_dot(k[:2], p), 0).reshape((-1, 1)) 188 | p -= _vec(p[:, 0].clamp(-k[2] * r, k[2] * r), torch.zeros_like(p[:, 0]) + r) 189 | return _length(p) * torch.sign(p[:, 1]) 190 | 191 | return f 192 | 193 | 194 | @sdf2 195 | def rounded_x(w_np, r_np): 196 | def f(p): 197 | w, r = tu.to_torch(p, w_np, r_np) 198 | p = p.abs() 199 | q = (_min(p[:, 0] + p[:, 1], w) * 0.5).reshape((-1, 1)) 200 | return _length(p - q) - r 201 | 202 | return f 203 | 204 | 205 | @sdf2 206 | def polygon(points): 207 | points_np = [np.array(p) for p in points] 208 | 209 | def f(p): 210 | points = [tu.to_torch(p, point) for point in points_np] 211 | n = len(points) 212 | d = _dot(p - points[0], p - points[0]) 213 | s = torch.ones_like(p[:, 0]) 214 | for i in range(n): 215 | j = (i + n - 1) % n 216 | vi = points[i] 217 | vj = points[j] 218 | e = vj - vi 219 | w = p - vi 220 | b = w - e * (torch.mv(w, e) / torch.dot(e, e)).clamp(0, 1).reshape((-1, 1)) 221 | d = _min(d, _dot(b, b)) 222 | c1 = p[:, 1] >= vi[1] 223 | c2 = p[:, 1] < vj[1] 224 | c3 = e[0] * w[:, 1] > e[1] * w[:, 0] 225 | c = _vec(c1, c2, c3) 226 | s = torch.where(torch.all(c, axis=1) | torch.all(~c, axis=1), -s, s) 227 | return s * d.sqrt() 228 | 229 | return f 230 | 231 | 232 | # Positioning 233 | 234 | 235 | @op2 236 | def translate(other, offset_np): 237 | def f(p): 238 | return other(p - tu.to_torch(p, offset_np)) 239 | 240 | return f 241 | 242 | 243 | @op2 244 | def scale(other, factor): 245 | try: 246 | x, y = factor 247 | except TypeError: 248 | x = y = factor 249 | s = (x, y) 250 | m = min(x, y) 251 | 252 | def f(p): 253 | return other(p / tu.to_torch(p, s)) * tu.to_torch(p, m) 254 | 255 | return f 256 | 257 | 258 | @op2 259 | def rotate(other, angle): 260 | s = np.sin(angle) 261 | c = np.cos(angle) 262 | m = 1 - c 263 | matrix = np.array([[c, -s], [s, c]]).T 264 | 265 | def f(p): 266 | return other(p @ tu.to_torch(p, matrix)) 267 | 268 | return f 269 | 270 | 271 | @op2 272 | def circular_array(other, count): 273 | angles = [i / count * 2 * np.pi for i in range(count)] 274 | return union(*[other.rotate(a) for a in angles]) 275 | 276 | 277 | # Alterations 278 | 279 | 280 | @op2 281 | def elongate(other, size): 282 | def f(p): 283 | q = p.abs() - tu.to_torch(p, size) 284 | x = q[:, 0].reshape((-1, 1)) 285 | y = q[:, 1].reshape((-1, 1)) 286 | w = _min(_max(x, y), 0) 287 | return other(_max(q, 0)) + w 288 | 289 | return f 290 | 291 | 292 | # 2D => 3D Operations 293 | 294 | 295 | @op23 296 | def extrude(other, h): 297 | def f(p): 298 | d = other(p[:, [0, 1]]) 299 | w = _vec(d.reshape(-1), p[:, 2].abs() - h / 2) 300 | return _min(_max(w[:, 0], w[:, 1]), 0) + _length(_max(w, 0)) 301 | 302 | return f 303 | 304 | 305 | @op23 306 | def extrude_to(a, b, h, e=ease.linear): 307 | def f(p): 308 | d1 = a(p[:, [0, 1]]) 309 | d2 = b(p[:, [0, 1]]) 310 | t = e((p[:, 2] / h).clamp(-0.5, 0.5) + 0.5) 311 | d = d1 + (d2 - d1) * t.reshape((-1, 1)) 312 | w = _vec(d.reshape(-1), p[:, 2].abs() - h / 2) 313 | return _min(_max(w[:, 0], w[:, 1]), 0) + _length(_max(w, 0)) 314 | 315 | return f 316 | 317 | 318 | @op23 319 | def revolve(other, offset=0): 320 | def f(p): 321 | xy = p[:, [0, 1]] 322 | q = _vec(_length(xy) - offset, p[:, 2]) 323 | return other(q) 324 | 325 | return f 326 | 327 | 328 | # Common 329 | 330 | union = op2(dn.union) 331 | difference = op2(dn.difference) 332 | intersection = op2(dn.intersection) 333 | blend = op2(dn.blend) 334 | negate = op2(dn.negate) 335 | dilate = op2(dn.dilate) 336 | erode = op2(dn.erode) 337 | shell = op2(dn.shell) 338 | repeat = op2(dn.repeat) 339 | -------------------------------------------------------------------------------- /sdf/d3.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import numpy as np 3 | import torch 4 | 5 | from . import dn, d2, ease, mesh, torch_util as tu 6 | 7 | # Constants 8 | 9 | ORIGIN = np.array((0, 0, 0)) 10 | 11 | X = np.array((1, 0, 0)) 12 | Y = np.array((0, 1, 0)) 13 | Z = np.array((0, 0, 1)) 14 | 15 | UP = Z 16 | 17 | # SDF Class 18 | 19 | _ops = {} 20 | 21 | 22 | class SDF3: 23 | def __init__(self, f): 24 | self.f = f 25 | 26 | def __call__(self, p): 27 | return self.f(p).reshape((-1, 1)) 28 | 29 | def __getattr__(self, name): 30 | if name in _ops: 31 | f = _ops[name] 32 | return functools.partial(f, self) 33 | raise AttributeError 34 | 35 | def __or__(self, other): 36 | return union(self, other) 37 | 38 | def __and__(self, other): 39 | return intersection(self, other) 40 | 41 | def __sub__(self, other): 42 | return difference(self, other) 43 | 44 | def k(self, k=None): 45 | self._k = k 46 | return self 47 | 48 | def generate(self, *args, **kwargs): 49 | return mesh.generate(self, *args, **kwargs) 50 | 51 | def save(self, path, *args, **kwargs): 52 | return mesh.save(path, self, *args, **kwargs) 53 | 54 | def show_slice(self, *args, **kwargs): 55 | return mesh.show_slice(self, *args, **kwargs) 56 | 57 | 58 | def sdf3(f): 59 | def wrapper(*args, **kwargs): 60 | return SDF3(f(*args, **kwargs)) 61 | 62 | return wrapper 63 | 64 | 65 | def op3(f): 66 | def wrapper(*args, **kwargs): 67 | return SDF3(f(*args, **kwargs)) 68 | 69 | _ops[f.__name__] = wrapper 70 | return wrapper 71 | 72 | 73 | def op32(f): 74 | def wrapper(*args, **kwargs): 75 | return d2.SDF2(f(*args, **kwargs)) 76 | 77 | _ops[f.__name__] = wrapper 78 | return wrapper 79 | 80 | 81 | # Helpers 82 | 83 | 84 | def _length(a): 85 | return torch.linalg.norm(a, dim=1) 86 | 87 | 88 | def _normalize_np(a): 89 | return a / np.linalg.norm(a) 90 | 91 | 92 | def _dot(a, b): 93 | return (a * b).sum(1) 94 | 95 | 96 | def _perpendicular(v): 97 | if v[1] == 0 and v[2] == 0: 98 | if v[0] == 0: 99 | raise ValueError("zero vector") 100 | else: 101 | return np.cross(v, [0, 1, 0]) 102 | return np.cross(v, [1, 0, 0]) 103 | 104 | 105 | _vec = tu.vec 106 | _min = tu.torch_min 107 | _max = tu.torch_max 108 | 109 | # Primitives 110 | 111 | 112 | @sdf3 113 | def sphere(radius=1, center=ORIGIN): 114 | def f(p): 115 | return _length(p - tu.to_torch(p, center)) - radius 116 | 117 | return f 118 | 119 | 120 | @sdf3 121 | def plane(normal=UP, point=ORIGIN): 122 | normal = _normalize_np(normal) 123 | 124 | def f(p): 125 | return torch.mv(tu.to_torch(p, point) - p, tu.to_torch(p, normal)) 126 | 127 | return f 128 | 129 | 130 | @sdf3 131 | def slab(x0=None, y0=None, z0=None, x1=None, y1=None, z1=None, k=None): 132 | fs = [] 133 | if x0 is not None: 134 | fs.append(plane(X, (x0, 0, 0))) 135 | if x1 is not None: 136 | fs.append(plane(-X, (x1, 0, 0))) 137 | if y0 is not None: 138 | fs.append(plane(Y, (0, y0, 0))) 139 | if y1 is not None: 140 | fs.append(plane(-Y, (0, y1, 0))) 141 | if z0 is not None: 142 | fs.append(plane(Z, (0, 0, z0))) 143 | if z1 is not None: 144 | fs.append(plane(-Z, (0, 0, z1))) 145 | return intersection(*fs, k=k) 146 | 147 | 148 | @sdf3 149 | def box(size=1, center=ORIGIN, a=None, b=None): 150 | if a is not None and b is not None: 151 | a = np.array(a) 152 | b = np.array(b) 153 | size = b - a 154 | center = a + size / 2 155 | return box(size, center) 156 | size = np.array(size) 157 | 158 | def f(p): 159 | q = (p - tu.to_torch(p, center)) - tu.to_torch(p, size / 2) 160 | return _length(_max(q, 0)) + _min(q.amax(1), 0) 161 | 162 | return f 163 | 164 | 165 | @sdf3 166 | def rounded_box(size, radius): 167 | size = np.array(size) 168 | 169 | def f(p): 170 | q = p.abs() - tu.to_torch(p, size / 2 + radius) 171 | return _length(_max(q, 0)) + _min(q.amax(1), 0) - radius 172 | 173 | return f 174 | 175 | 176 | @sdf3 177 | def wireframe_box(size_np, thickness_np): 178 | size_np = np.array(size_np) 179 | 180 | def g(a, b, c): 181 | return _length(_max(_vec(a, b, c), 0)) + _min(_max(a, _max(b, c)), 0) 182 | 183 | def f(p): 184 | size, thickness = tu.to_torch(p, size_np, thickness_np) 185 | p = p.abs() - size / 2 - thickness / 2 186 | q = (p + thickness / 2).abs() - thickness / 2 187 | px, py, pz = p[:, 0], p[:, 1], p[:, 2] 188 | qx, qy, qz = q[:, 0], q[:, 1], q[:, 2] 189 | return _min(_min(g(px, qy, qz), g(qx, py, qz)), g(qx, qy, pz)) 190 | 191 | return f 192 | 193 | 194 | @sdf3 195 | def torus(r1, r2): 196 | def f(p): 197 | xy = p[:, [0, 1]] 198 | z = p[:, 2] 199 | a = _length(xy) - r1 200 | b = _length(_vec(a, z)) - r2 201 | return b 202 | 203 | return f 204 | 205 | 206 | @sdf3 207 | def capsule(a_np, b_np, radius): 208 | a_np = np.array(a_np) 209 | b_np = np.array(b_np) 210 | 211 | def f(p): 212 | a, b = tu.to_torch(p, a_np, b_np) 213 | pa = p - a 214 | ba = b - a 215 | h = (torch.mv(pa, ba) / torch.dot(ba, ba)).clamp(0, 1).reshape((-1, 1)) 216 | return _length(pa - (ba * h)) - radius 217 | 218 | return f 219 | 220 | 221 | @sdf3 222 | def cylinder(radius): 223 | def f(p): 224 | return _length(p[:, [0, 1]]) - radius 225 | 226 | return f 227 | 228 | 229 | @sdf3 230 | def capped_cylinder(a_np, b_np, radius): 231 | a_np = np.array(a_np) 232 | b_np = np.array(b_np) 233 | 234 | def f(p): 235 | a, b = tu.to_torch(p, a_np, b_np) 236 | ba = b - a 237 | pa = p - a 238 | baba = torch.dot(ba, ba) 239 | paba = torch.mv(pa, ba).reshape((-1, 1)) 240 | x = _length(pa * baba - ba * paba) - radius * baba 241 | y = (paba - baba * 0.5).abs() - baba * 0.5 242 | x = x.reshape((-1, 1)) 243 | y = y.reshape((-1, 1)) 244 | x2 = x * x 245 | y2 = y * y * baba 246 | d = torch.where( 247 | _max(x, y) < 0, 248 | -_min(x2, y2), 249 | torch.where(x > 0, x2, 0) + torch.where(y > 0, y2, 0), 250 | ) 251 | return torch.sign(d) * d.abs().sqrt() / baba 252 | 253 | return f 254 | 255 | 256 | @sdf3 257 | def rounded_cylinder(ra, rb, h): 258 | def f(p): 259 | d = _vec(_length(p[:, [0, 1]]) - ra + rb, p[:, 2].abs() - h / 2 + rb) 260 | return _min(_max(d[:, 0], d[:, 1]), 0) + _length(_max(d, 0)) - rb 261 | 262 | return f 263 | 264 | 265 | @sdf3 266 | def capped_cone(a_np, b_np, ra, rb): 267 | a_np = np.array(a_np) 268 | b_np = np.array(b_np) 269 | 270 | def f(p): 271 | a, b = tu.to_torch(p, a_np, b_np) 272 | rba = rb - ra 273 | baba = torch.dot(b - a, b - a) 274 | papa = _dot(p - a, p - a) 275 | paba = torch.mv(p - a, b - a) / baba 276 | x = (papa - paba * paba * baba).sqrt() 277 | cax = _max(0, x - torch.where(paba < 0.5, ra, rb)) 278 | cay = (paba - 0.5).abs() - 0.5 279 | k = rba * rba + baba 280 | f = ((rba * (x - ra) + paba * baba) / k).clamp(0, 1) 281 | cbx = x - ra - f * rba 282 | cby = paba - f 283 | s = torch.where(torch.logical_and(cbx < 0, cay < 0), -1, 1) 284 | return ( 285 | s * _min(cax * cax + cay * cay * baba, cbx * cbx + cby * cby * baba).sqrt() 286 | ) 287 | 288 | return f 289 | 290 | 291 | @sdf3 292 | def rounded_cone(r1, r2, h): 293 | def f(p): 294 | q = _vec(_length(p[:, [0, 1]]), p[:, 2]) 295 | b = (r1 - r2) / h 296 | a = np.sqrt(1 - b * b) 297 | k = q @ _vec(-b, a) 298 | c1 = _length(q) - r1 299 | c2 = _length(q - _vec(0, h)) - r2 300 | c3 = (q @ _vec(a, b)) - r1 301 | return torch.where(k < 0, c1, torch.where(k > a * h, c2, c3)) 302 | 303 | return f 304 | 305 | 306 | @sdf3 307 | def ellipsoid(size_np): 308 | size_np = np.array(size_np) 309 | 310 | def f(p): 311 | size = tu.to_torch(p, size_np) 312 | k0 = _length(p / size) 313 | k1 = _length(p / (size * size)) 314 | return k0 * (k0 - 1) / k1 315 | 316 | return f 317 | 318 | 319 | @sdf3 320 | def pyramid(h): 321 | def f(p): 322 | a = p[:, [0, 1]].abs() - 0.5 323 | w = a[:, 1] > a[:, 0] 324 | a[w] = a[:, [1, 0]][w] 325 | px = a[:, 0] 326 | py = p[:, 2] 327 | pz = a[:, 1] 328 | m2 = h * h + 0.25 329 | qx = pz 330 | qy = h * py - 0.5 * px 331 | qz = h * px + 0.5 * py 332 | s = _max(-qx, 0) 333 | t = ((qy - 0.5 * pz) / (m2 + 0.25)).clamp(0, 1) 334 | a = m2 * (qx + s) ** 2 + qy * qy 335 | b = m2 * (qx + 0.5 * t) ** 2 + (qy - m2 * t) ** 2 336 | d2 = torch.where(_min(qy, -qx * m2 - qy * 0.5) > 0, 0, _min(a, b)) 337 | return ((d2 + qz * qz) / m2).sqrt() * torch.sign(_max(qz, -py)) 338 | 339 | return f 340 | 341 | 342 | # Platonic Solids 343 | 344 | 345 | @sdf3 346 | def tetrahedron(r): 347 | def f(p): 348 | x = p[:, 0] 349 | y = p[:, 1] 350 | z = p[:, 2] 351 | return (_max((x + y).abs() - z, (x - y).abs() + z) - 1) / np.sqrt(3) 352 | 353 | return f 354 | 355 | 356 | @sdf3 357 | def octahedron(r): 358 | def f(p): 359 | return (p.abs().sum(1) - r) * np.tan(np.radians(30)) 360 | 361 | return f 362 | 363 | 364 | @sdf3 365 | def dodecahedron(r): 366 | x, y, z = _normalize_np(((1 + np.sqrt(5)) / 2, 1, 0)) 367 | 368 | def f(p): 369 | p = (p / r).abs() 370 | a = torch.mv(p, tu.vec(x, y, z).to(p)) 371 | b = torch.mv(p, tu.vec(z, x, y).to(p)) 372 | c = torch.mv(p, tu.vec(y, z, x).to(p)) 373 | q = (_max(_max(a, b), c) - x) * r 374 | return q 375 | 376 | return f 377 | 378 | 379 | @sdf3 380 | def icosahedron(r): 381 | r *= 0.8506507174597755 382 | x, y, z = _normalize_np(((np.sqrt(5) + 3) / 2, 1, 0)) 383 | w = np.sqrt(3) / 3 384 | 385 | def f(p): 386 | p = (p / r).abs() 387 | a = torch.mv(p, tu.vec(x, y, z).to(p)) 388 | b = torch.mv(p, tu.vec(z, x, y).to(p)) 389 | c = torch.mv(p, tu.vec(y, z, x).to(p)) 390 | d = torch.mv(p, tu.vec(w, w, w).to(p)) - x 391 | return _max(_max(_max(a, b), c) - x, d) * r 392 | 393 | return f 394 | 395 | 396 | # Positioning 397 | 398 | 399 | @op3 400 | def translate(other, offset_np): 401 | def f(p): 402 | offset = tu.to_torch(p, offset_np) 403 | return other(p - offset) 404 | 405 | return f 406 | 407 | 408 | @op3 409 | def scale(other, factor): 410 | try: 411 | x, y, z = factor 412 | except TypeError: 413 | x = y = z = factor 414 | s = (x, y, z) 415 | m = min(x, min(y, z)) 416 | 417 | def f(p): 418 | return other(p / tu.to_torch(p, s)) * tu.to_torch(p, m) 419 | 420 | return f 421 | 422 | 423 | @op3 424 | def rotate(other, angle, vector=Z): 425 | x, y, z = _normalize_np(vector) 426 | s = np.sin(angle) 427 | c = np.cos(angle) 428 | m = 1 - c 429 | matrix = np.array( 430 | [ 431 | [m * x * x + c, m * x * y + z * s, m * z * x - y * s], 432 | [m * x * y - z * s, m * y * y + c, m * y * z + x * s], 433 | [m * z * x + y * s, m * y * z - x * s, m * z * z + c], 434 | ] 435 | ).T 436 | 437 | def f(p): 438 | return other(p @ tu.to_torch(p, matrix)) 439 | 440 | return f 441 | 442 | 443 | @op3 444 | def rotate_to(other, a, b): 445 | a = _normalize_np(np.array(a)) 446 | b = _normalize_np(np.array(b)) 447 | dot = np.dot(b, a) 448 | if dot == 1: 449 | return other 450 | if dot == -1: 451 | return rotate(other, np.pi, _perpendicular(a)) 452 | angle = np.arccos(dot) 453 | v = _normalize_np(np.cross(b, a)) 454 | return rotate(other, angle, v) 455 | 456 | 457 | @op3 458 | def orient(other, axis): 459 | return rotate_to(other, UP, axis) 460 | 461 | 462 | @op3 463 | def circular_array(other, count, offset): 464 | other = other.translate(X * offset) 465 | da = 2 * np.pi / count 466 | 467 | def f(p): 468 | x = p[:, 0] 469 | y = p[:, 1] 470 | z = p[:, 2] 471 | d = torch.hypot(x, y) 472 | a = torch.atan2(y, x) % da 473 | d1 = other(_vec(torch.cos(a - da) * d, torch.sin(a - da) * d, z)) 474 | d2 = other(_vec(torch.cos(a) * d, torch.sin(a) * d, z)) 475 | return _min(d1, d2) 476 | 477 | return f 478 | 479 | 480 | # Alterations 481 | 482 | 483 | @op3 484 | def elongate(other, size): 485 | def f(p): 486 | q = p.abs() - tu.to_torch(p, size) 487 | x = q[:, 0].reshape((-1, 1)) 488 | y = q[:, 1].reshape((-1, 1)) 489 | z = q[:, 2].reshape((-1, 1)) 490 | w = _min(_max(x, _max(y, z)), 0) 491 | return other(_max(q, 0)) + w 492 | 493 | return f 494 | 495 | 496 | @op3 497 | def twist(other, k): 498 | def f(p): 499 | x = p[:, 0] 500 | y = p[:, 1] 501 | z = p[:, 2] 502 | c = torch.cos(k * z) 503 | s = torch.sin(k * z) 504 | x2 = c * x - s * y 505 | y2 = s * x + c * y 506 | z2 = z 507 | return other(_vec(x2, y2, z2)) 508 | 509 | return f 510 | 511 | 512 | @op3 513 | def bend(other, k): 514 | def f(p): 515 | x = p[:, 0] 516 | y = p[:, 1] 517 | z = p[:, 2] 518 | c = torch.cos(k * x) 519 | s = torch.sin(k * x) 520 | x2 = c * x - s * y 521 | y2 = s * x + c * y 522 | z2 = z 523 | return other(_vec(x2, y2, z2)) 524 | 525 | return f 526 | 527 | 528 | @op3 529 | def bend_linear(other, p0_np, p1_np, v_np, e=ease.linear): 530 | p0_np = np.array(p0_np) 531 | p1_np = np.array(p1_np) 532 | v_np = -np.array(v_np) 533 | ab_np = p1_np - p0_np 534 | 535 | def f(p): 536 | p0, v, ab = tu.to_torch(p, p0_np, v_np, ab_np) 537 | t = (((p - p0) @ ab) / (ab @ ab)).clamp(0, 1) 538 | t = e(t).reshape((-1, 1)) 539 | return other(p + t * v) 540 | 541 | return f 542 | 543 | 544 | @op3 545 | def bend_radial(other, r0_np, r1_np, dz_np, e=ease.linear): 546 | def f(p): 547 | r0, r1, dz = tu.to_torch(p, r0_np, r1_np, dz_np) 548 | x = p[:, 0] 549 | y = p[:, 1] 550 | z = p[:, 2] 551 | r = torch.hypot(x, y) 552 | t = ((r - r0) / (r1 - r0)).clamp(0, 1) 553 | z = z - dz * e(t) 554 | return other(_vec(x, y, z)) 555 | 556 | return f 557 | 558 | 559 | @op3 560 | def transition_linear(f0, f1, p0_np=-Z, p1_np=Z, e=ease.linear): 561 | p0_np = np.array(p0_np) 562 | p1_np = np.array(p1_np) 563 | ab_np = p1_np - p0_np 564 | 565 | def f(p): 566 | p0, ab = tu.to_torch(p, p0_np, ab_np) 567 | d1 = f0(p) 568 | d2 = f1(p) 569 | t = np.clip(np.dot(p - p0, ab) / np.dot(ab, ab), 0, 1) 570 | t = e(t).reshape((-1, 1)) 571 | return t * d2 + (1 - t) * d1 572 | 573 | return f 574 | 575 | 576 | @op3 577 | def transition_radial(f0, f1, r0=0, r1=1, e=ease.linear): 578 | def f(p): 579 | d1 = f0(p) 580 | d2 = f1(p) 581 | r = torch.hypot(p[:, 0], p[:, 1]) 582 | t = ((r - r0) / (r1 - r0)).clamp(0, 1) 583 | t = e(t).reshape((-1, 1)) 584 | return t * d2 + (1 - t) * d1 585 | 586 | return f 587 | 588 | 589 | @op3 590 | def wrap_around(other, x0, x1, r=None, e=ease.linear): 591 | p0_np = X * x0 592 | p1_np = X * x1 593 | v_np = Y 594 | if r is None: 595 | r = np.linalg.norm(p1_np - p0_np) / (2 * np.pi) 596 | 597 | def f(p): 598 | p0, p1, v = tu.to_torch(p, p0_np, p1_np, v_np) 599 | x = p[:, 0] 600 | y = p[:, 1] 601 | z = p[:, 2] 602 | d = np.hypot(x, y) - r 603 | d = d.reshape((-1, 1)) 604 | a = np.arctan2(y, x) 605 | t = (a + np.pi) / (2 * np.pi) 606 | t = e(t).reshape((-1, 1)) 607 | q = p0 + (p1 - p0) * t + v * d 608 | q[:, 2] = z 609 | return other(q) 610 | 611 | return f 612 | 613 | 614 | # 3D => 2D Operations 615 | 616 | 617 | @op32 618 | def slice(other): 619 | # TODO: support specifying a slice plane 620 | # TODO: probably a better way to do this 621 | s = slab(z0=-1e-9, z1=1e-9) 622 | a = other & s 623 | b = other.negate() & s 624 | 625 | def f(p): 626 | p = _vec(p[:, 0], p[:, 1], torch.zeros_like(p[:, 0])) 627 | A = a(p).reshape(-1) 628 | B = -b(p).reshape(-1) 629 | w = A <= 0 630 | A[w] = B[w] 631 | return A 632 | 633 | return f 634 | 635 | 636 | # Common 637 | 638 | union = op3(dn.union) 639 | difference = op3(dn.difference) 640 | intersection = op3(dn.intersection) 641 | blend = op3(dn.blend) 642 | negate = op3(dn.negate) 643 | dilate = op3(dn.dilate) 644 | erode = op3(dn.erode) 645 | shell = op3(dn.shell) 646 | repeat = op3(dn.repeat) 647 | -------------------------------------------------------------------------------- /sdf/dn.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import torch 3 | import numpy as np 4 | 5 | from . import torch_util as tu 6 | 7 | _min = tu.torch_min 8 | _max = tu.torch_max 9 | 10 | 11 | def union(a, *bs, k=None): 12 | def f(p): 13 | d1 = a(p) 14 | for b in bs: 15 | d2 = b(p) 16 | K = k or getattr(b, "_k", None) 17 | if K is None: 18 | d1 = _min(d1, d2) 19 | else: 20 | h = (0.5 + 0.5 * (d2 - d1) / K).clamp(0, 1) 21 | m = d2 + (d1 - d2) * h 22 | d1 = m - K * h * (1 - h) 23 | return d1 24 | 25 | return f 26 | 27 | 28 | def difference(a, *bs, k=None): 29 | def f(p): 30 | d1 = a(p) 31 | for b in bs: 32 | d2 = b(p) 33 | K = k or getattr(b, "_k", None) 34 | if K is None: 35 | d1 = _max(d1, -d2) 36 | else: 37 | h = (0.5 - 0.5 * (d2 + d1) / K).clamp(0, 1) 38 | m = d1 + (-d2 - d1) * h 39 | d1 = m + K * h * (1 - h) 40 | return d1 41 | 42 | return f 43 | 44 | 45 | def intersection(a, *bs, k=None): 46 | def f(p): 47 | d1 = a(p) 48 | for b in bs: 49 | d2 = b(p) 50 | K = k or getattr(b, "_k", None) 51 | if K is None: 52 | d1 = _max(d1, d2) 53 | else: 54 | h = (0.5 - 0.5 * (d2 - d1) / K).clamp(0, 1) 55 | m = d2 + (d1 - d2) * h 56 | d1 = m + K * h * (1 - h) 57 | return d1 58 | 59 | return f 60 | 61 | 62 | def blend(a, *bs, k=0.5): 63 | def f(p): 64 | d1 = a(p) 65 | for b in bs: 66 | d2 = b(p) 67 | K = k or getattr(b, "_k", None) 68 | d1 = K * d2 + (1 - K) * d1 69 | return d1 70 | 71 | return f 72 | 73 | 74 | def negate(other): 75 | def f(p): 76 | return -other(p) 77 | 78 | return f 79 | 80 | 81 | def dilate(other, r): 82 | def f(p): 83 | return other(p) - r 84 | 85 | return f 86 | 87 | 88 | def erode(other, r): 89 | def f(p): 90 | return other(p) + r 91 | 92 | return f 93 | 94 | 95 | def shell(other, thickness): 96 | def f(p): 97 | return other(p).abs() - thickness / 2 98 | 99 | return f 100 | 101 | 102 | def repeat(other, spacing, count=None, padding=0): 103 | count = np.array(count) if count is not None else None 104 | spacing = np.array(spacing) 105 | 106 | def neighbors(dim, padding, spacing): 107 | try: 108 | padding = [padding[i] for i in range(dim)] 109 | except Exception: 110 | padding = [padding] * dim 111 | try: 112 | spacing = [spacing[i] for i in range(dim)] 113 | except Exception: 114 | spacing = [spacing] * dim 115 | for i, s in enumerate(spacing): 116 | if s == 0: 117 | padding[i] = 0 118 | axes = [list(range(-p, p + 1)) for p in padding] 119 | return list(itertools.product(*axes)) 120 | 121 | def f(p): 122 | spacing_th = tu.to_torch(p, spacing) 123 | q = p / torch.where( 124 | spacing_th == 0, torch.ones_like(spacing_th), spacing_th 125 | ).to(p) 126 | if count is None: 127 | index = q.round() 128 | else: 129 | count_th = tu.to_torch(p, count) 130 | index = q.round().clamp(-count_th, count_th) 131 | 132 | offsets = neighbors(p.shape[-1], padding, spacing) 133 | indices = torch.cat([index + tu.to_torch(index, n) for n in offsets]) 134 | A = other(p.repeat(len(offsets), 1) - spacing_th * indices).view( 135 | len(offsets), -1 136 | ) 137 | return A.min(0)[0] 138 | 139 | return f 140 | -------------------------------------------------------------------------------- /sdf/ease.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | 4 | 5 | def linear(t): 6 | return t 7 | 8 | 9 | def in_quad(t): 10 | return t * t 11 | 12 | 13 | def out_quad(t): 14 | return -t * (t - 2) 15 | 16 | 17 | def in_out_quad(t): 18 | u = 2 * t - 1 19 | a = 2 * t * t 20 | b = -0.5 * (u * (u - 2) - 1) 21 | return torch.where(t < 0.5, a, b) 22 | 23 | 24 | def in_cubic(t): 25 | return t * t * t 26 | 27 | 28 | def out_cubic(t): 29 | u = t - 1 30 | return u * u * u + 1 31 | 32 | 33 | def in_out_cubic(t): 34 | u = t * 2 35 | v = u - 2 36 | a = 0.5 * u * u * u 37 | b = 0.5 * (v * v * v + 2) 38 | return torch.where(u < 1, a, b) 39 | 40 | 41 | def in_quart(t): 42 | return t * t * t * t 43 | 44 | 45 | def out_quart(t): 46 | u = t - 1 47 | return -(u * u * u * u - 1) 48 | 49 | 50 | def in_out_quart(t): 51 | u = t * 2 52 | v = u - 2 53 | a = 0.5 * u * u * u * u 54 | b = -0.5 * (v * v * v * v - 2) 55 | return torch.where(u < 1, a, b) 56 | 57 | 58 | def in_quint(t): 59 | return t * t * t * t * t 60 | 61 | 62 | def out_quint(t): 63 | u = t - 1 64 | return u * u * u * u * u + 1 65 | 66 | 67 | def in_out_quint(t): 68 | u = t * 2 69 | v = u - 2 70 | a = 0.5 * u * u * u * u * u 71 | b = 0.5 * (v * v * v * v * v + 2) 72 | return torch.where(u < 1, a, b) 73 | 74 | 75 | def in_sine(t): 76 | return -torch.cos(t * np.pi / 2) + 1 77 | 78 | 79 | def out_sine(t): 80 | return torch.sin(t * np.pi / 2) 81 | 82 | 83 | def in_out_sine(t): 84 | return -0.5 * (torch.cos(np.pi * t) - 1) 85 | 86 | 87 | def in_expo(t): 88 | a = torch.zeros_like(t) 89 | b = 2 ** (10 * (t - 1)) 90 | return torch.where(t == 0, a, b) 91 | 92 | 93 | def out_expo(t): 94 | a = torch.ones_like(t) 95 | b = 1 - 2 ** (-10 * t) 96 | return torch.where(t == 1, a, b) 97 | 98 | 99 | def in_out_expo(t): 100 | zero = torch.zeros_like(t) 101 | one = zero + 1 102 | a = 0.5 * 2 ** (20 * t - 10) 103 | b = 1 - 0.5 * 2 ** (-20 * t + 10) 104 | return torch.where( 105 | t == 0, zero, torch.where(t == 1, one, torch.where(t < 0.5, a, b)) 106 | ) 107 | 108 | 109 | def in_circ(t): 110 | return -1 * (torch.sqrt(1 - t * t) - 1) 111 | 112 | 113 | def out_circ(t): 114 | u = t - 1 115 | return torch.sqrt(1 - u * u) 116 | 117 | 118 | def in_out_circ(t): 119 | u = t * 2 120 | v = u - 2 121 | a = -0.5 * (torch.sqrt(1 - u * u) - 1) 122 | b = 0.5 * (torch.sqrt(1 - v * v) + 1) 123 | return torch.where(u < 1, a, b) 124 | 125 | 126 | def in_elastic(t, k=0.5): 127 | u = t - 1 128 | return -1 * (2 ** (10 * u) * torch.sin((u - k / 4) * (2 * np.pi) / k)) 129 | 130 | 131 | def out_elastic(t, k=0.5): 132 | return 2 ** (-10 * t) * torch.sin((t - k / 4) * (2 * np.pi / k)) + 1 133 | 134 | 135 | def in_out_elastic(t, k=0.5): 136 | u = t * 2 137 | v = u - 1 138 | a = -0.5 * (2 ** (10 * v) * torch.sin((v - k / 4) * 2 * np.pi / k)) 139 | b = 2 ** (-10 * v) * torch.sin((v - k / 4) * 2 * np.pi / k) * 0.5 + 1 140 | return torch.where(u < 1, a, b) 141 | 142 | 143 | def in_back(t): 144 | k = 1.70158 145 | return t * t * ((k + 1) * t - k) 146 | 147 | 148 | def out_back(t): 149 | k = 1.70158 150 | u = t - 1 151 | return u * u * ((k + 1) * u + k) + 1 152 | 153 | 154 | def in_out_back(t): 155 | k = 1.70158 * 1.525 156 | u = t * 2 157 | v = u - 2 158 | a = 0.5 * (u * u * ((k + 1) * u - k)) 159 | b = 0.5 * (v * v * ((k + 1) * v + k) + 2) 160 | return torch.where(u < 1, a, b) 161 | 162 | 163 | def in_bounce(t): 164 | return 1 - out_bounce(1 - t) 165 | 166 | 167 | def out_bounce(t): 168 | a = (121 * t * t) / 16 169 | b = (363 / 40 * t * t) - (99 / 10 * t) + 17 / 5 170 | c = (4356 / 361 * t * t) - (35442 / 1805 * t) + 16061 / 1805 171 | d = (54 / 5 * t * t) - (513 / 25 * t) + 268 / 25 172 | return torch.where( 173 | t < 4 / 11, a, torch.where(t < 8 / 11, b, torch.where(t < 9 / 10, c, d)) 174 | ) 175 | 176 | 177 | def in_out_bounce(t): 178 | a = in_bounce(2 * t) * 0.5 179 | b = out_bounce(2 * t - 1) * 0.5 + 0.5 180 | return torch.where(t < 0.5, a, b) 181 | 182 | 183 | def in_square(t): 184 | a = torch.zeros_like(t) 185 | b = a + 1 186 | return torch.where(t < 1, a, b) 187 | 188 | 189 | def out_square(t): 190 | a = torch.zeros_like(t) 191 | b = a + 1 192 | return torch.where(t > 0, b, a) 193 | 194 | 195 | def in_out_square(t): 196 | a = torch.zeros_like(t) 197 | b = a + 1 198 | return torch.where(t < 0.5, a, b) 199 | 200 | 201 | def _main(): 202 | import matplotlib.pyplot as plt 203 | 204 | fs = [ 205 | linear, 206 | in_quad, 207 | out_quad, 208 | in_out_quad, 209 | in_cubic, 210 | out_cubic, 211 | in_out_cubic, 212 | in_quart, 213 | out_quart, 214 | in_out_quart, 215 | in_quint, 216 | out_quint, 217 | in_out_quint, 218 | in_sine, 219 | out_sine, 220 | in_out_sine, 221 | in_expo, 222 | out_expo, 223 | in_out_expo, 224 | in_circ, 225 | out_circ, 226 | in_out_circ, 227 | in_elastic, 228 | out_elastic, 229 | in_out_elastic, 230 | in_back, 231 | out_back, 232 | in_out_back, 233 | in_bounce, 234 | out_bounce, 235 | in_out_bounce, 236 | in_square, 237 | out_square, 238 | in_out_square, 239 | ] 240 | x = torch.linspace(0, 1, 1000) 241 | for f in fs: 242 | y = f(x) 243 | plt.plot(x.numpy(), y.numpy(), label=f.__name__) 244 | plt.legend() 245 | plt.show() 246 | 247 | 248 | if __name__ == "__main__": 249 | _main() 250 | -------------------------------------------------------------------------------- /sdf/mesh.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from multiprocessing.pool import ThreadPool 3 | from skimage import measure 4 | 5 | import multiprocessing 6 | import itertools 7 | import numpy as np 8 | import torch 9 | import time 10 | 11 | from . import progress, stl 12 | 13 | WORKERS = multiprocessing.cpu_count() 14 | SAMPLES = 2 ** 22 15 | BATCH_SIZE = 32 16 | 17 | 18 | def _marching_cubes(volume, level=0): 19 | verts, faces, _, _ = measure.marching_cubes(volume, level) 20 | return verts[faces].reshape((-1, 3)) 21 | 22 | 23 | def _cartesian_product(*arrays): 24 | la = len(arrays) 25 | dtype = np.result_type(*arrays) 26 | arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) 27 | for i, a in enumerate(np.ix_(*arrays)): 28 | arr[..., i] = a 29 | return arr.reshape(-1, la) 30 | 31 | 32 | def _skip(sdf, job): 33 | X, Y, Z = job 34 | x0, x1 = X[0], X[-1] 35 | y0, y1 = Y[0], Y[-1] 36 | z0, z1 = Z[0], Z[-1] 37 | x = (x0 + x1) / 2 38 | y = (y0 + y1) / 2 39 | z = (z0 + z1) / 2 40 | r = abs(sdf(torch.tensor([(x, y, z)])).item()) 41 | d = np.linalg.norm(np.array((x - x0, y - y0, z - z0))) 42 | if r <= d: 43 | return False 44 | corners = np.array(list(itertools.product((x0, x1), (y0, y1), (z0, z1)))) 45 | values = sdf(torch.from_numpy(corners)).reshape(-1).numpy() 46 | same = np.all(values > 0) if values[0] > 0 else np.all(values < 0) 47 | return same 48 | 49 | 50 | def _worker(sdf, job, sparse): 51 | X, Y, Z = job 52 | if sparse and _skip(sdf, job): 53 | return None 54 | # return _debug_triangles(X, Y, Z) 55 | P = _cartesian_product(X, Y, Z) 56 | # Use a GPU if it is available. 57 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 58 | volume = ( 59 | sdf(torch.from_numpy(P).to(device)) 60 | .reshape((len(X), len(Y), len(Z))) 61 | .cpu() 62 | .numpy() 63 | ) 64 | try: 65 | points = _marching_cubes(volume) 66 | except Exception: 67 | return [] 68 | # return _debug_triangles(X, Y, Z) 69 | scale = np.array([X[1] - X[0], Y[1] - Y[0], Z[1] - Z[0]]) 70 | offset = np.array([X[0], Y[0], Z[0]]) 71 | return points * scale + offset 72 | 73 | 74 | def _estimate_bounds(sdf): 75 | # TODO: raise exception if bound estimation fails 76 | s = 16 77 | x0 = y0 = z0 = -1e9 78 | x1 = y1 = z1 = 1e9 79 | prev = None 80 | for i in range(32): 81 | X = np.linspace(x0, x1, s) 82 | Y = np.linspace(y0, y1, s) 83 | Z = np.linspace(z0, z1, s) 84 | d = np.array([X[1] - X[0], Y[1] - Y[0], Z[1] - Z[0]]) 85 | threshold = np.linalg.norm(d) / 2 86 | if threshold == prev: 87 | break 88 | prev = threshold 89 | P = _cartesian_product(X, Y, Z) 90 | volume = sdf(torch.from_numpy(P)).reshape((len(X), len(Y), len(Z))).numpy() 91 | where = np.argwhere(np.abs(volume) <= threshold) 92 | x1, y1, z1 = (x0, y0, z0) + where.max(axis=0) * d + d / 2 93 | x0, y0, z0 = (x0, y0, z0) + where.min(axis=0) * d - d / 2 94 | return ((x0, y0, z0), (x1, y1, z1)) 95 | 96 | 97 | def generate( 98 | sdf, 99 | step=None, 100 | bounds=None, 101 | samples=SAMPLES, 102 | workers=WORKERS, 103 | batch_size=BATCH_SIZE, 104 | verbose=True, 105 | sparse=True, 106 | ): 107 | 108 | start = time.time() 109 | 110 | if bounds is None: 111 | bounds = _estimate_bounds(sdf) 112 | (x0, y0, z0), (x1, y1, z1) = bounds 113 | 114 | if step is None and samples is not None: 115 | volume = (x1 - x0) * (y1 - y0) * (z1 - z0) 116 | step = (volume / samples) ** (1 / 3) 117 | 118 | try: 119 | dx, dy, dz = step 120 | except TypeError: 121 | dx = dy = dz = step 122 | 123 | if verbose: 124 | print("min %g, %g, %g" % (x0, y0, z0)) 125 | print("max %g, %g, %g" % (x1, y1, z1)) 126 | print("step %g, %g, %g" % (dx, dy, dz)) 127 | 128 | X = np.arange(x0, x1, dx) 129 | Y = np.arange(y0, y1, dy) 130 | Z = np.arange(z0, z1, dz) 131 | 132 | s = batch_size 133 | Xs = [X[i : i + s + 1] for i in range(0, len(X), s)] 134 | Ys = [Y[i : i + s + 1] for i in range(0, len(Y), s)] 135 | Zs = [Z[i : i + s + 1] for i in range(0, len(Z), s)] 136 | 137 | batches = list(itertools.product(Xs, Ys, Zs)) 138 | num_batches = len(batches) 139 | num_samples = sum(len(xs) * len(ys) * len(zs) for xs, ys, zs in batches) 140 | 141 | if verbose: 142 | print( 143 | "%d samples in %d batches with %d workers" 144 | % (num_samples, num_batches, workers) 145 | ) 146 | 147 | points = [] 148 | skipped = empty = nonempty = 0 149 | bar = progress.Bar(num_batches, enabled=verbose) 150 | pool = ThreadPool(workers) 151 | f = partial(_worker, sdf, sparse=sparse) 152 | for result in pool.imap(f, batches): 153 | bar.increment(1) 154 | if result is None: 155 | skipped += 1 156 | elif len(result) == 0: 157 | empty += 1 158 | else: 159 | nonempty += 1 160 | points.extend(result) 161 | bar.done() 162 | 163 | if verbose: 164 | print("%d skipped, %d empty, %d nonempty" % (skipped, empty, nonempty)) 165 | triangles = len(points) // 3 166 | seconds = time.time() - start 167 | print("%d triangles in %g seconds" % (triangles, seconds)) 168 | 169 | return points 170 | 171 | 172 | def save(path, *args, **kwargs): 173 | points = generate(*args, **kwargs) 174 | stl.write_binary_stl(path, points) 175 | 176 | 177 | def _debug_triangles(X, Y, Z): 178 | x0, x1 = X[0], X[-1] 179 | y0, y1 = Y[0], Y[-1] 180 | z0, z1 = Z[0], Z[-1] 181 | 182 | p = 0.25 183 | x0, x1 = x0 + (x1 - x0) * p, x1 - (x1 - x0) * p 184 | y0, y1 = y0 + (y1 - y0) * p, y1 - (y1 - y0) * p 185 | z0, z1 = z0 + (z1 - z0) * p, z1 - (z1 - z0) * p 186 | 187 | v = [ 188 | (x0, y0, z0), 189 | (x0, y0, z1), 190 | (x0, y1, z0), 191 | (x0, y1, z1), 192 | (x1, y0, z0), 193 | (x1, y0, z1), 194 | (x1, y1, z0), 195 | (x1, y1, z1), 196 | ] 197 | 198 | return [ 199 | v[3], 200 | v[5], 201 | v[7], 202 | v[5], 203 | v[3], 204 | v[1], 205 | v[0], 206 | v[6], 207 | v[4], 208 | v[6], 209 | v[0], 210 | v[2], 211 | v[0], 212 | v[5], 213 | v[1], 214 | v[5], 215 | v[0], 216 | v[4], 217 | v[5], 218 | v[6], 219 | v[7], 220 | v[6], 221 | v[5], 222 | v[4], 223 | v[6], 224 | v[3], 225 | v[7], 226 | v[3], 227 | v[6], 228 | v[2], 229 | v[0], 230 | v[3], 231 | v[2], 232 | v[3], 233 | v[0], 234 | v[1], 235 | ] 236 | 237 | 238 | def sample_slice(sdf, w=1024, h=1024, x=None, y=None, z=None, bounds=None): 239 | 240 | if bounds is None: 241 | bounds = _estimate_bounds(sdf) 242 | (x0, y0, z0), (x1, y1, z1) = bounds 243 | 244 | if x is not None: 245 | X = np.array([x]) 246 | Y = np.linspace(y0, y1, w) 247 | Z = np.linspace(z0, z1, h) 248 | extent = (Z[0], Z[-1], Y[0], Y[-1]) 249 | axes = "ZY" 250 | elif y is not None: 251 | Y = np.array([y]) 252 | X = np.linspace(x0, x1, w) 253 | Z = np.linspace(z0, z1, h) 254 | extent = (Z[0], Z[-1], X[0], X[-1]) 255 | axes = "ZX" 256 | elif z is not None: 257 | Z = np.array([z]) 258 | X = np.linspace(x0, x1, w) 259 | Y = np.linspace(y0, y1, h) 260 | extent = (Y[0], Y[-1], X[0], X[-1]) 261 | axes = "YX" 262 | else: 263 | raise Exception("x, y, or z position must be specified") 264 | 265 | P = _cartesian_product(X, Y, Z) 266 | return sdf(P).reshape((w, h)), extent, axes 267 | 268 | 269 | def show_slice(*args, **kwargs): 270 | import matplotlib.pyplot as plt 271 | 272 | show_abs = kwargs.pop("abs", False) 273 | a, extent, axes = sample_slice(*args, **kwargs) 274 | if show_abs: 275 | a = np.abs(a) 276 | im = plt.imshow(a, extent=extent, origin="lower") 277 | plt.xlabel(axes[0]) 278 | plt.ylabel(axes[1]) 279 | plt.colorbar(im) 280 | plt.show() 281 | -------------------------------------------------------------------------------- /sdf/progress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | 5 | def pretty_time(seconds): 6 | seconds = int(round(seconds)) 7 | s = seconds % 60 8 | m = (seconds // 60) % 60 9 | h = seconds // 3600 10 | return "%d:%02d:%02d" % (h, m, s) 11 | 12 | 13 | class Bar(object): 14 | def __init__(self, max_value=100, min_value=0, enabled=True): 15 | self.min_value = min_value 16 | self.max_value = max_value 17 | self.value = min_value 18 | self.start_time = time.time() 19 | self.enabled = enabled 20 | 21 | @property 22 | def percent_complete(self): 23 | t = (self.value - self.min_value) / (self.max_value - self.min_value) 24 | return t * 100 25 | 26 | @property 27 | def elapsed_time(self): 28 | return time.time() - self.start_time 29 | 30 | @property 31 | def eta(self): 32 | t = self.percent_complete / 100 33 | if t == 0: 34 | return 0 35 | return (1 - t) * self.elapsed_time / t 36 | 37 | def increment(self, delta): 38 | self.update(self.value + delta) 39 | 40 | def update(self, value): 41 | self.value = value 42 | if self.enabled: 43 | sys.stdout.write(" %s \r" % self.render()) 44 | sys.stdout.flush() 45 | 46 | def done(self): 47 | self.update(self.max_value) 48 | self.stop() 49 | 50 | def stop(self): 51 | if self.enabled: 52 | sys.stdout.write("\n") 53 | sys.stdout.flush() 54 | 55 | def render(self): 56 | items = [ 57 | self.render_percent_complete(), 58 | self.render_value(), 59 | self.render_bar(), 60 | self.render_elapsed_time(), 61 | self.render_eta(), 62 | ] 63 | return " ".join(items) 64 | 65 | def render_percent_complete(self): 66 | return "%3.0f%%" % self.percent_complete 67 | 68 | def render_value(self): 69 | if self.min_value == 0: 70 | return "(%g of %g)" % (self.value, self.max_value) 71 | else: 72 | return "(%g)" % (self.value) 73 | 74 | def render_bar(self, size=30): 75 | a = int(round(self.percent_complete / 100.0 * size)) 76 | b = size - a 77 | return "[" + "#" * a + "-" * b + "]" 78 | 79 | def render_elapsed_time(self): 80 | return pretty_time(self.elapsed_time) 81 | 82 | def render_eta(self): 83 | return pretty_time(self.eta) 84 | -------------------------------------------------------------------------------- /sdf/stl.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import struct 3 | 4 | 5 | def write_binary_stl(path, points): 6 | n = len(points) // 3 7 | 8 | points = np.array(points, dtype="float32").reshape((-1, 3, 3)) 9 | normals = np.cross(points[:, 1] - points[:, 0], points[:, 2] - points[:, 0]) 10 | normals /= np.linalg.norm(normals, axis=1).reshape((-1, 1)) 11 | 12 | dtype = np.dtype( 13 | [("normal", ("= tw - 1) | (j < 0) | (j >= th - 1) 96 | d[outside] = q[outside] 97 | return torch.from_numpy(d).to(old_p) 98 | 99 | return f 100 | 101 | 102 | def _bilinear_interpolate(a, x, y): 103 | x0 = np.floor(x).astype(int) 104 | x1 = x0 + 1 105 | y0 = np.floor(y).astype(int) 106 | y1 = y0 + 1 107 | 108 | x0 = np.clip(x0, 0, a.shape[1] - 1) 109 | x1 = np.clip(x1, 0, a.shape[1] - 1) 110 | y0 = np.clip(y0, 0, a.shape[0] - 1) 111 | y1 = np.clip(y1, 0, a.shape[0] - 1) 112 | 113 | pa = a[y0, x0] 114 | pb = a[y1, x0] 115 | pc = a[y0, x1] 116 | pd = a[y1, x1] 117 | 118 | wa = (x1 - x) * (y1 - y) 119 | wb = (x1 - x) * (y - y0) 120 | wc = (x - x0) * (y1 - y) 121 | wd = (x - x0) * (y - y0) 122 | 123 | return wa * pa + wb * pb + wc * pc + wd * pd 124 | -------------------------------------------------------------------------------- /sdf/torch_util.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | import numpy as np 4 | import torch 5 | 6 | 7 | def to_torch(ref_tensor: torch.Tensor, *objs: Any) -> torch.Tensor: 8 | if len(objs) == 1: 9 | return _to_torch(ref_tensor, objs[0]) 10 | return tuple(_to_torch(ref_tensor, x) for x in objs) 11 | 12 | 13 | def _to_torch(ref_tensor: torch.Tensor, obj: Any) -> torch.Tensor: 14 | if isinstance(obj, torch.Tensor): 15 | return obj.to(ref_tensor) 16 | return _to_torch(ref_tensor, torch.from_numpy(np.array(obj))) 17 | 18 | 19 | def vec(*xs: Union[torch.Tensor, float]) -> torch.Tensor: 20 | if isinstance(xs[0], torch.Tensor): 21 | return torch.stack(xs, axis=-1) 22 | return torch.tensor(list(xs)) 23 | 24 | 25 | def torch_max( 26 | x1: Union[torch.Tensor, float], x2: Union[torch.Tensor, float] 27 | ) -> Union[torch.Tensor, float]: 28 | if not isinstance(x1, torch.Tensor) and not isinstance(x2, torch.Tensor): 29 | return max(x1, x2) 30 | elif not isinstance(x1, torch.Tensor): 31 | return x2.clamp(min=x1) 32 | elif not isinstance(x2, torch.Tensor): 33 | return x1.clamp(min=x2) 34 | return torch.maximum(x1, x2) 35 | 36 | 37 | def torch_min( 38 | x1: Union[torch.Tensor, float], x2: Union[torch.Tensor, float] 39 | ) -> Union[torch.Tensor, float]: 40 | if not isinstance(x1, torch.Tensor) and not isinstance(x2, torch.Tensor): 41 | return min(x1, x2) 42 | elif not isinstance(x1, torch.Tensor): 43 | return x2.clamp(max=x1) 44 | elif not isinstance(x2, torch.Tensor): 45 | return x1.clamp(max=x2) 46 | return torch.minimum(x1, x2) 47 | -------------------------------------------------------------------------------- /sdf/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | pi = math.pi 4 | 5 | degrees = math.degrees 6 | radians = math.radians 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="sdf", 5 | version="0.1", 6 | description="Generate 3D meshes from signed distance functions.", 7 | author="Michael Fogleman", 8 | author_email="michael.fogleman@gmail.com", 9 | packages=["sdf"], 10 | install_requires=["numpy", "scikit-image", "scipy", "Pillow"], 11 | license="MIT", 12 | classifiers=( 13 | "Development Status :: 3 - Alpha", 14 | "Intended Audience :: Developers", 15 | "Natural Language :: English", 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | ), 20 | ) 21 | --------------------------------------------------------------------------------