| Home | zeGraph lib | Lua lib | Custom lib | Tutorials | Notes | XML Script | C-Talk | Z-Script |
Lesson 1
Lesson 2
Lesson 3
Lesson 4
Lesson 5
Lesson 6
Lesson 7
Lesson 8
Lesson 9
Lesson 10
Lesson 11
Lesson 12
Lesson 13
Lesson 14
Lesson 15
Lesson 16
Lesson 17
Lesson 18
Lesson 19

Lesson 18 3D Plot

In principle there is no difference between 2D and 3D plots for zeGraph, whereas in practice, the tow types are handled quite differently because of the the rotation operation of a 3D plot.

By default the z-axis of the zePlot object is disabled. A call to its set() function with the argument of type = "z" will enable the axis. Similar to the situation of 2D plot, you relocate the z-axis either through the anchor option of the set() function or through the offset option by calculating the z-offset as

z_offset = - z_scale * max(image_width, image_height) / 2

You should also rotate the plot so that its three axes and the whole view appear properly. Text after rotation may not look satisfactory. If so, you have to suppress axis and tick labels and then add your own text to the plot with the (x, y, z) coordinate to specify the position to anchor the test.

Surface Plot

In the discussion for zeExpat object, We have shown how to use it to parse data in a xml file. We further show here how to create a 3D surface plot from co2.xml, which contains the global air CO2 distribution from the World Data Centre for Greenhouse Gases.

require("register")

xml = zeAux.new("xml")
xml:callback("f1", "f2", "f3");

ivec = 0
tag = nil
info = { }	    --save tags and array info in this table

-- Start tag callback

f1 = function(n, s, t)
    tag = s
    info[s] = 1
    if (s == "data" and info.air_co2) then
        arr = zeUtl.new("double")
        arr:resize(tonumber(info.nrow), tonumber(info.ncol))
    end
end

-- End tag callback

f2 = function(n, s)
    tag = nil
end

-- Data callback

f3 = function(n, s, l)
    if (not xml.isblank(s)) then
        if (tag) then
            if (tag ~= "data" ) then
                info[tag] = s
                tag = nil
            else
                if (info.air_co2) then
                    arr:parse(ivec, s)
                    ivec = ivec + 1
                end
            end
        end
    end
end

-- Read lines from the file and parse them

for line in io.lines("co2.xml") do
    xml:parse(line)
end


arr:delete(0)   -- delete the vector containing year
arr:delete(0)   -- delete the vector containing month
arr:flip()      -- make latutide from south to north
arr:transpose() -- time as x and latitude as y

-- Data are too dense with time. Get rid of some.

co2, idx = zeUtl.new("double", "uint")
idx:range(0, 2, 114)
arr:sample(idx, co2)

-- Construct the surface

xyz, ctb, vec = zeUtl.new("double", "double", "double")

zeMake.grid2quads(xyz, 1, co2)
zeMake.normal4(co2, 10, xyz)

ctb:resize(5, 3)
ctb:fill(0)
ctb:setele(0, 2, 1)
ctb:setele(1, 2, 1)
ctb:setele(1, 1, .9)
ctb:setele(2, 1, 1)
ctb:setele(3, 0, 1)
ctb:setele(3, 1, .9)
ctb:setele(4, 0, 1)

xyz:getarr(2, vec)
zeMake.data2color(arr, vec, ctb)

render, scene, root, light, blend, plot, font
    = zeGrf.new("render", "scene", "node",
       "light", "blend", "plot", "font")
    
render:add(scene)
scene:set{node = root}
root:add(light, blend, plot)
light:set{position = {-500, -500, 500},
          ambient = {0.5, 0.5 , 0.5, 1}}

plot:font(font)
plot:rotate(20, -50)
plot:scale(0.6, 0.4, 0.3)
plot:set{axis = "x", range = {0, 113},
         ticklabels = {"83|85|87|89|91|93|95|97|99|01", false},
         tickmarks = {0, 12, 1}, ticklength = 1.5, linewidth = 1.5,
         label = "Year", smooth = true, offset = {0, -102, -77}}

plot:set{axis = "y", range = {0, 8},
         ticklabels = {"80S|60S|40S|20S|0|20N|40N|60N|80N", 0},
         tickmarks = {0, 1, 0}, ticklength = 1.5, linewidth = 1.5,
         label = "Latitude", smooth = true, offset = {-154, 0, -77}}

plot:set{axis = "z", range = {330, 380},
         tickmarks = {340, 10, 1}, ticklength = 1.5, linewidth = 1.5,
         label = "CO2 (ppm)", smooth = true, offset = {-154, 102, 0}}

shape1, shape2, vertex, normal, color
    = zeGrf.new("polygon", "polygon", "vertex", "vertex", "color")
    
shape1:set{vertex = vertex, vertex_normal = normal,
          vertex_color = color, type = "quads"}

shape2:set{vertex = vertex, type = "quads", fill = 0,
           smooth = true, color = {0, .5, .5, 1, 1}}
shape2:translate(0, 0, 0.1)

plot:add(shape1)
plot:add(shape2)

vertex:add(xyz)
normal:add(co2)
color:add(arr)

root:translate(20, 0, 0)

render:tofile("co2.png")

co2.png

Pie Graph

The pie graph presented here is not so sophisticated, but it gives the idea of how produce a free-layout 3D graph without using the zePlot object. There are several functions in lib3d.lua for creating commonly used 3D shapes for business and scientific plots. The pie_3d() is a good example for customizing shape creation.

require("register")

render, scene, root, node, light
    = zeGrf.new("render", "scene", "node", "node", "light")
render:add(scene)
scene:set{node = root}
root:add(light, node)
light:set{position = {500, 500, 500}}

require("pie_3d")
pie = pie_3d(100, 20, 30, 10, 310)
node:add(pie)
pie:set{color = {0, 1, 1, 1}}

pie2 = pie_3d(100, 20, 15, 310, 370)
node:add(pie2)
pie2:translate(10, -5, 0)
pie2:set{color = {1, .5, 0, 1}}

text, text2, font = zeGrf.new("text", "text", "font")

text:font(font)
text:text("83.3%")
text:set{color = {0, 0, 0, 1},
        layout = {-70, 20, 150,
                 -70+1, 20, 150,
                 -70, 20+1, 150}}

text2:font(font)
text2:text("16.7%")
text2:set{color = {0, 0, 0, 1},
          layout = {50, -10, 150,
                    50+1, -10, 150,
                    50, -10+1, 150}}

root:add(text, text2)

node:rotatex(-60)

render:tofile("pie.png")

pie.png

Spring

Learning to glue various shapes together is the first step toward creating sophisticated ones. There are two distinguished ways to composed shapes: (1) create many parts of a shape, transform them, and then add them to a node; and (2) create a building block of shapes, create many nodes, add the block to then, and then transform the node. Now let's construct a spring by the second method.

require("register")

render, scene, node, light
      = zeGrf.new("render", "scene", "node", "light")
render:add(scene)
scene:set{node = node}
node:add(light)
light:set{position = {50, 300, 500}}

require("pipe_3d")
r = 10; h = 15
pipe = pipe_3d(r, h, 16)

R = 100
pi = 0.017453293		        -- 2 * pi / 360
x0 = -R; y0 = 0; z0 = 0

require("trans_3d")

for k = 0, 5 do
    for angle = 0, 355, 5 do
        shapenode = zeGrf.new("node")
        shapenode:add(pipe)
        node:add(shapenode)

        x1 = R * math.cos(pi * angle)
        y1 = R * math.sin(pi * angle)
        z1 = z0 + .5;

        --This function calculate translation distance and
        --rotation angles.
        
        ds, theta, phi = trans_3d(x0, y0, z0, x1, y1, z1)

        shapenode:rotatey(theta)
        shapenode:rotatez(phi)
        shapenode:translate(x0, y0, z0)

        x0 = x1; y0 = y1; z0 = z1
    end
end

node:translate(0, 0, -100)
node:rotatex(-60)
node:set{color = {0, .7, 1, 1}}

render:tofile("spring.png")

spring.png

Using the same technique, you can lay the pipes along any path to construct such things like ropes. If the turning angle of a joint is too sharp, you can add a ball shape at the joint. Further more, you can use texture or material objects to make the shape looks cool and realistic.

Knot

Actually when the curvatures of the path along which you plan to lay objects are very sharp, it is probably better to use only balls. The knot function we used in the following example comes again from Paul Bourke' web site.

require("register")

render, scene, node, light
        = zeGrf.new("render", "scene", "node", "light")
render:add(scene)
scene:set{node = node}
node:add(light)
light:set{position = {50, 150, 150}}

--Notice this change of light color.

light:set{ambient = {0, .4, .4, 1}}

pi2 = 6.2831853
nele = 1000
r = 2

step = 2    --The image on the right is produce with step = 10

require("ball_3d")

for i = 0, nele, step do
    mu = i * pi2 / nele;
    x1 = 10 * (math.cos(mu) + math.cos(3*mu)) + 
          math.cos(2*mu) + math.cos(4*mu)
    y1 = 6 * math.sin(mu) + 10 * math.sin(3*mu)
    z1 = 4 * math.sin(3*mu) * math.sin(5*mu/2) + 
          4*math.sin(4*mu) - 2 * math.sin(6*mu)

    if (i ~= 0) then
        ball = ball_3d(r, 3)
        ball:translate(x1, y1, z1)
        node:add(ball)
    end     

    x0 = x1;
    y0 = y1;
    z0 = z1;
end

node:set{color = {0, 1, 1, 1}}
node:scale(6, 6, 6)

render:tofile("knot.png")

knot.png

Methane

Now let's make something educational. We chose to construct a methane molecule for the simplicity of its structure. Here it is:

require("register")

render, scene, root, light
     = zeGrf.new("render", "scene", "node", "light")
render:add(scene)
scene:set{node = root}
scene:perspective(4)
root:add(light)
light:set{position = {120, 120, 120}}

--Use ball and bar shapes to represent hydrogen atom and bond.

require("pipe_3d")
require("ball_3d")
h = 200
bar = pipe_3d(10, h, 16)
bar:set{color = {.2, .6, 0, 1}}
ball = ball_3d(40, 4)
ball:set{color = {0, .8, .8, 1}}

--Node for manipulating the molecule.
-- It also contains the carbon atom.

ball2 = ball_3d(60, 4)
ball2:set{color = {0.9, 0, 0, 1}}
node0 = zeGrf.new("node")
node0:add(ball2)
root:add(node0)

--Hydrogen and bond

node1, node2, node3, node4
    = zeGrf.new("node", "node", "node", "node")
node0:add(node1, node2, node3, node4)

node1:add(bar, ball)
node1:translate(0, 0, -h)
node1:rotatex(35.3)
node2:add(bar, ball)
node2:translate(0, 0, -h)
node2:rotatex(144.7)
node3:add(bar, ball)
node3:translate(0, 0, -h)
node3:rotatex(-35.3)
node3:rotatey(90)

node4:add(bar, ball)
node4:translate(0, 0, -h)
node4:rotatex(-35.3)
node4:rotatey(-90)

node0:rotatey(45)

render:tofile("methane.png")

methane.png