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()
is an attempt at a simple hidden-surface display. In real 3D software, this process has to be far more sophisticated.
eyepoint(10, 10, 10)
perspective(3000)
cube = make(Cube, "cube")
pin(cube)
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 = (o) -> poly(objecttopoly(o)[1], :stroke))
The objecttopoly()
function returns a tuple, containing the 2D vertices, and the polygons that define the faces.
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)
setline(0.5)
for (n, o) in enumerate([Cube, Tetrahedron, Pyramid, Teapot])
@layer begin
translate(first.(t)[n])
object = make(o, string(o))
scaleby!(object, 80, 80, 80)
pin(object)
end
end
helloworld()
axes3D(200)
teapot = make(Teapot)
setline(0.5)
scaleby!(teapot, 100, 100, 100)
pin(teapot, gfunction=wireframe)
You can load a few more objects by including the moreobjects.jl
file:
include(dirname(dirname(pathof(Thebes))) * "/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.
Using gfunctions
You do this with a gfunction.
Here's a simple example:
include(dirname(pathof(Thebes)) * "/../data/moreobjects.jl")
setlinejoin("bevel")
eyepoint(150, 150, 150)
function mygfunction(o::Object)
cols = [Luxor.julia_green, Luxor.julia_red, Luxor.julia_purple, Luxor.julia_blue]
sortfaces!(o)
if !isempty(o.faces)
@layer begin
for (n, face) in enumerate(o.faces)
@layer begin
vertices = o.vertices[face]
sn = surfacenormal(vertices)
ang = anglebetweenvectors(sn, eyepoint())
sethue(cols[mod1(n, end)])
pin(vertices, gfunction = (p3, p2) ->
begin
poly(p2, :fill)
sethue("gold")
poly(p2, :stroke, close=true)
end)
end
end
end
end
setcolor("gold3")
pin.(o.vertices, gfunction = (p3, p2) -> begin
setopacity(1)
circle(p2, 2, :fill)
end)
end
object = make(geodesic, "geodesic")
setopacity(0.9)
setline(0.5)
pin(scaleby!(object, 200, 200, 200), gfunction = mygfunction)
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.
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 used above can do this for simple objects - it may be sufficient.
Using custom code
Thebes.jl is a work in progress, and a good general-purpose rendering function that draws everything with lots of optional parameters is not yet provided. However, you can avoid using the built-in pin(o::Object)
function, and experiment with code such as the following:
using Luxor, Thebes, Colors, ColorSchemes
include(dirname(pathof(Thebes)) * "/../data/moreobjects.jl")
function lighten(col::Colorant, f)
c = convert(RGB, col)
return RGB(f * c.r, f* c.g, f * c.b)
end
function drawobject(o;
color=colorant"red")
setlinejoin("bevel")
if !isempty(o.faces)
@layer begin
for (n, f) in enumerate(o.faces)
vs = o.vertices[f]
sn = surfacenormal(vs)
ang = anglebetweenvectors(sn, eyepoint())
sl = slope(O, vs[1])
sethue(lighten(color, rescale(ang, 0, π, -2, 2)))
pin(vs, gfunction = (p3, p2) -> begin
poly(p2, :fill)
sethue("grey30")
poly(p2, :stroke)
end)
end
end
end
end
function sphere(size, origin, color)
s1 = make(sphere2)
scaleby!(s1, size, size, size)
moveby!(s1, origin)
sortfaces!(s1)
drawobject(s1, color=color)
end
function main()
Drawing(500, 500, "assets/figures/juliaspheres.svg")
background("grey20")
origin()
helloworld()
eyepoint(300, 300, 300)
perspective(450)
setline(.5)
sphere(90, Point3D(150, 0, 0), RGB(Luxor.julia_red...))
sphere(90, Point3D(0, 150, 0), RGB(Luxor.julia_purple...))
sphere(90, Point3D(0, 0, 150), RGB(Luxor.julia_green...))
finish()
end
main()
nothing # hide
This code uses the surface normal of each rectangular facet to change the color. The surface normal is an imaginary line that meets the facet at right angles, and indicates the direction of that facet. If you measure the distance between the surface normal and the direction of, say, the direction of a line from the origin to the eyepoint, you can obtain a value that indicates the orientation of the facet. You can then use this, as here, to change the color: an angle approaching π suggests that the facet is almost facing the viewer, and you can color it accordingly.
It's hard work doing it all like this! There are easier ways...