├── .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 | |  |  |  |  |
54 | |  |  |  |  |
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 | 
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 |
--------------------------------------------------------------------------------