Objects

So far we've been drawing individual points and lines. This gets tiresome when you have a lot of them. Fortunately, Thebes has a few features for handling larger groups of points.

Making objects

You make a 3D object using make(), and then use pin() to throw it onto the 2D drawing.

make() expects an array of 3D points, an (optional) array of face definitions, and an (optional) array of labels, plus an (optional) name. These arrays let you link faces with vertices. It returns an Object.

A Cube object is already defined in Thebes (we needn't have made one earlier, really). So after:

cube = make(Cube, "cube")

the cube variable contains:

Object(
    Point3D[
        Point3D(-0.5, 0.5, -0.5),
        Point3D(0.5, 0.5, -0.5),
        Point3D(0.5, -0.5, -0.5),
        Point3D(-0.5, -0.5, -0.5),
        Point3D(-0.5, 0.5, 0.5),
        Point3D(0.5, 0.5, 0.5),
        Point3D(0.5, -0.5, 0.5),
        Point3D(-0.5, -0.5, 0.5)
    ],

    [[1, 2, 3, 4],
     [2, 6, 7, 3],
     [6, 5, 8, 7],
     [5, 1, 4, 8],
     [1, 5, 6, 2],
     [4, 3, 7, 8]],

     [1, 2, 3, 4, 5, 6],

     "cube")

The default rendering applied by pin() uses less than fifty shades of grey to draw it.

eyepoint(10, 10, 10)
perspective(3000)
cube = make(Cube, "cube")
pin(cube)

simple cube object

Here's a very simple example of how you might make your own object from scratch.

tol = 0.001
a = Point3D[]
for t in -2pi:tol:2pi
    push!(a, Point3D((100 + cos(5t)) * cos(3t), (100 + cos(5t)) * sin(2t), sin(5t)))
end
sethue("darkorange")
knot = make([a, []], "knot")

pin(knot, gfunction = (args...) -> poly(args[1], :stroke))

point example

The gfunction here receives the vertices, faces, and labels in args, but the faces and labels are empty, so this simple use of pin only needs to draw a polygon through the vertices.

Warning

The gfunction used by pin() for objects doesn't give you access to the 3D points, unlike the versions used for points.

OFF the shelf objects

Obviously this isn't something you'd want to do "by hand" very often. Fortunately there are plenty of people who are prepared to make 3D objects and distribute them in standard file formats, via the internet. Thebes.jl knows about one of these formats, the Object File Format (.OFF). So there are a few objects already available for you to use directly.

Using objects

The following objects are preloaded (from data/objects.jl) when Thebes.jl starts:

  • Cube
  • Tetrahedron
  • Pyramid
  • Teapot
t = Tiler(600, 300, 2, 2)
for (n, o) in enumerate([Cube, Tetrahedron, Pyramid, Teapot])
    @layer begin
        translate(first.(t)[n])
        object = make(o, string(o))
        setscale!(object, 50, 50, 50)
        pin(object)
    end
end

more objects

@svg begin
    helloworld()
    perspective(1600)
    axes3D()
    teapot = make(Teapot)
    sortfaces!(teapot)
    setscale!(teapot, 15, 15, 15)
    setopacity(0.5)
    pin(teapot)
end

teapot

You can load a few more objects by including the moreobjects.jl file:

include("data/moreobjects.jl")

which brings these objects into play:

boxcube boxtorus concave cone crossshape cube cuboctahedron dodecahedron geodesic helix2 icosahedron icosidodecahedron octahedron octtorus rhombicosidodecahedron rhombicuboctahedron rhombitruncated_cubeoctahedron rhombitruncated_icosidodecahedron snub_cube snub_dodecahedron sphere2 tet3d tetrahedron triangle truncated_cube truncated_dodecahedron truncated_icosahedron truncated_octahedron truncated_tetrahedron

Rendering objects

To render objects, there are many choices you can make about how to draw the faces and the vertices. You do this with a gfunction. For objects, the gfunction is more complex than for points and lines. It takes lists of vertices, faces, and labels.

include(dirname(pathof(Thebes)) * "/../data/moreobjects.jl")

setlinejoin("bevel")
eyepoint(150, 150, 150)

function mygfunction(vertices, faces, labels; action=:fill)
    cols = [Luxor.julia_green, Luxor.julia_red, Luxor.julia_purple, Luxor.julia_blue]

    if !isempty(faces)
        @layer begin
        for (n, p) in enumerate(faces)

            @layer begin
                sethue(cols[mod1(n, end)])
                poly(p, close = true, action)
            end

            sethue("white")
            setline(0.5)
            poly(p, :stroke, close=true)

            end
        end
    end
    setcolor("gold")
    circle.(vertices, 2, :fill)
end

setopacity(0.7)
object = make(geodesic, "geodesic")
sortfaces!(object)
pin(setscale!(object, 200, 200, 200), gfunction = mygfunction)

geodesic

Faces

The faces are drawn in the order in which they were defined. But to be a more realistic 3D drawing, the faces should be drawn so that the ones nearest the viewer are drawn last, or better still, so that the ones that can't be seen aren't drawn at all.

Note

This is why Thebes is more of a wireframe tool than any kind of genuine 3D application. Use Makie.jl. Or program Blender with Julia.

In theory it's possible to do some quick calculations on an object to sort the faces into the correct order for a particular viewpoint. The sortfaces!() function tries to do that. For simple objects this may be sufficient.

function mygfunction(vertices, faces, labels; action=:fill)
    cols = [Luxor.julia_green, Luxor.julia_red, Luxor.julia_purple, Luxor.julia_blue]
    if !isempty(faces)
        @layer begin
            for (n, p) in enumerate(faces)

                @layer begin
                    sethue(cols[mod1(n, end)])
                    poly(p, close = true, action)
                end

                sethue("white")
                setline(0.5)
                poly(p, :stroke, close=true)

            end
        end
    end
end

background("black")
origin()
setlinejoin("bevel")
eyepoint(Point3D(150, 150, 150))
perspective(0)
axes3D(20)

object = make(Cube, "cube")
setscale!(object, 100, 100, 100)

# draw as is
setposition!(object, Point3D(0, -200, 0))
pin(object, gfunction = mygfunction)

# draw with sorted faces
setposition!(object, Point3D(0, 400, 0))
sortfaces!(object, eyepoint=eyepoint())
pin(object, gfunction = mygfunction)

object