GitHub - marceloprates/easyshader: Easy-to-use Signed Distance Field (SDF) library

3 min read Original article ↗

easyshader

easyshader is a tool for rendering 3D scenes, exporting .ply files for 3D printing and creating animations, powered by Signed Distance Fields (SDFs) and written in Python/Taichi.

It was created to enable drawing 3D shapes using a very concise syntax, and is packed with 3D primitives, transformations and smooth operators.

Run in Google Colab

Basic usage

# If you're running this from Google Colab or you simply don't have easyshader installed, you can install it by running:
#%pip install git+https://github.com/marceloprates/easyshader

from easyshader import *

png

Use the "color" parameter to paint your object

png

easyshader primitives

You can choose from the following primitives:

  • Box
  • BoxFrame
  • Callable
  • Cone
  • Cyllinder
  • Icosahedron
  • Iterable
  • Line
  • Number
  • Octahedron
  • Shape
  • Sphere
  • Torus
for obj in [Sphere(1), Cyllinder(1,1), Cone(1,2), Torus(1,.2), Box(1), BoxFrame(1,.1), Icosahedron(1), Octahedron(1)]:
    display(obj.paint('#0B4F6C'))

png

png

png

png

png

png

png

png

Exporting to .ply for usage in Blender or 3D printing

Export your creations to polygon meshes for 3d printing or rendering on external apps (e.g. Blender)

#Icosahedron(1).to_mesh(simplify = 20, save_path='icosahedron.obj')

Color your creations using functions defined over x,y,z and a color palette:

palette = ['#B80C09','#0B4F6C','#01BAEF','#FBFBFF','#040F16']

x = Box(.8,'palette(6*(x+y+z))',palette = palette)
x = x.isometric()

x

png

Binary operations

Union:

BoxFrame(1,.1,'#0B4F6C') + Sphere(.5,'#B80C09')

png

Difference:

Box(1,'#0B4F6C') - Sphere(1.2)

png

Intersection:

Icosahedron(1,'#0B4F6C') & Sphere(1.1)

png

Examples

A coffee cup!

x = Sphere(1, 'palette(6*(x+y+z))', palette = palette)
x = x.twist(4)

x &= Cyllinder(.5,.5)
x -= Cyllinder(.4,.5) + 'dy .1'
x += (Torus(.3,.05) & Shape('-x')) + 'dx .5'
x = x.isometric()
x += 'rx -pi/3'

x

png

Create videos!

Use the 't' (time) variable to control the animation

x = BoxFrame(1,.1,'palette(6*t + 6*(x+y+z))',palette = palette)
x += '.1*sin(t)'
x += 'ry t'
x += 'rx t'

x.animate(frames = 60, framerate = 15, iterations = 1000)
Animating..: 100%|██████████| 59/59 [08:59<00:00,  9.14s/it]

png

Transformations

Translation

png

png

Scale

png

png

Rotation

png

Advanced transformations

You can use x,y,z (and the time parameter, t) as variables in transformations such as translation, rotation, scale

BoxFrame(1,.1,'#f44') + 'dx .2*y'

png

Other operations

Twist along an axis

Box(1,'#f44').twist(2,'y')

png

Create an "onion" shape

# Create an onion from a box
x = Box(1,'#f44').onion()
# Cut a hole in the onion
x &= Shape('z')

x

png

Smooth operators

Smooth union

sphere = (Sphere(.5,'#f44') + 'dx -.5')
box = (Box(.5,'#4ff') + 'dx +.5')

# Normal union
display(sphere + box)

# Smooth union
display(sphere <<su(.5)>> box)

png

png

Smooth difference

sphere = Sphere(1.1)
box = Box(1,'#f44')

# Normal difference
display(box - sphere)

# Smooth difference
display(box <<sd(.5)>> sphere)

png

png

Smooth intersection

sphere = Sphere(1)
box = Box(.9,'#f44')

# Normal intersection
display(box & sphere)

# Smooth intersection
display(box <<si(.5)>> sphere)

png

png

Use custom taichi functions!

@ti.func
def mandelbulb_fn(p,max_it,k):
    z, dr, r = p, 1., 0.
    for i in range(max_it):
        r, steps = z.norm(), i
        if r > 4.0: break;
        # convert to polar coordinates
        theta = ti.acos(z.z/r)
        phi = ti.atan2(z.y,z.x)
        dr = r**2 * 3 * dr + 1.0
        # scale and rotate the point
        zr = r**3
        theta = theta*3
        phi = phi*3
        # convert back to cartesian coordinates
        z = zr*ti.Vector([
            ti.sin(theta)*ti.cos(phi),
            ti.sin(phi)*ti.sin(theta),
            ti.cos(theta)
        ])
        z+=p
    out = ti.log(r)*r/dr
    if k==1:
        out = r
    return out

# Create mandelbulb shape
mandelbulb = Shape(
    # Call 'mandelbulb_fn' to compute the SDF
    sdf = '.2*mandelbulb_fn(p,10,0)',
    #color = 'palette(200*mandelbulb_fn(1.1*p))',
    color = 'palette(12*mandelbulb_fn(p,10,1))',
    palette = ['#0C0F0A', '#FBFF12', '#FF206E', '#41EAD4', '#FFFFFF'],
    # Pass 'mandelbulb_fn' as a keyword argument
    mandelbulb_fn = mandelbulb_fn,
)

mandelbulb = mandelbulb.isometric()

# Only run the lines below if you have a very powerful GPU!

#mandelbulb.render(
#    resolution = (2480,3508),
#    fov = .25,
#    max_raymarch_steps = 200,
#    iterations = 1000,
#    verbose = True
#)
Rendering scene...: 100%|██████████| 1000/1000 [20:02<00:00,  1.20s/it]

png