| Home | Lua | XML | C-Talk | Z-Script |
Lua
OpenGL
Surface
Iso-surface
3D plot
Sub-plots

Surface Generator

The surface generator is a Lua function designed to generate surfaces of parametric equations:

    x = x(u, v)
    y = y(u, v)
    z = z(u, v)

It appears complicated but is actually very simple to do. However, generating correct vertex normals may take more work. Here is the generator function:

require("register")  -- register zeGraph libraries

function surface_generator(uarr, varr, func)
    local nu, nv = uarr:size(), varr:size()
    local xyz = zeGrf.new("vertex")
    
    local x1, y1, z1, x2, y2, z2 =
          zeUtl.new("double", "double", "double",
                    "double", "double", "double")
    
    x1:resize(nv,1); y1:resize(nv,1); z1:resize(nv,1)
    x2:resize(nv,1); y2:resize(nv,1); z2:resize(nv,1)

    -- calculate vertices for the first u and all v values    
    local u = uarr:getele(0,0)
    local x, y, z
    for i = 0, nv-1 do
        x, y, z = func(u, varr:getele(i, 0))
        x1:setele(i, 0, x); y1:setele(i, 0, y); z1:setele(i, 0, z)
    end
    
    for j = 1, nu-1 do
        -- calculate vertices for the next u and all v values
        u = uarr:getele(j,0)
        for i = 0, nv-1 do
            x, y, z = func(u, varr:getele(i, 0))
            x2:setele(i, 0, x); y2:setele(i, 0, y); z2:setele(i, 0, z)
        end
        -- construct the surface with quads
        for i = 1, nv-1 do
            xyz:add(x1:getele(i-1, 0), y1:getele(i-1, 0), z1:getele(i-1, 0))
            xyz:add(x2:getele(i-1, 0), y2:getele(i-1, 0), z2:getele(i-1, 0))
            xyz:add(x2:getele(i,   0), y2:getele(i,   0), z2:getele(i,   0))
            xyz:add(x1:getele(i,   0), y1:getele(i,   0), z1:getele(i,   0))
        end
        x2:copy(x1); y2:copy(y1); z2:copy(z1)
    end
        
    return xyz
end

1. Horn

The parametric equations for a horn is taken from the MathWorld:

    x = (2 + u*cos(v))*sin(2*pi*u)
    y = (2 + u*cos(v))*cos(2*pi*u) + 2*u
    z = u*sin(v)
    0 <= u <= 1
    0 <= v <= 2*pi

Their derivatives are

    x'|u = cos(v)*sin(2*pi*u) + 2*pi*(2 + u*cos(v))*cos(2*pi*u)
    y'|u = cos(v)*cos(2*pi*u) - 2*pi*(2 + u*cos(v))*sin(2*pi*u) + 2
    z'|u = sin(v)
    x'|v = -u*sin(v)*sin(2*pi*u)
    y'|v = -u*sin(v)*cos(2*pi*u)
    z'|v = u*cos(v)	

The zeMake package has a function to calculate the normal for a plane determined by three vertices of v0, v1, and v2; or two vectors of v1-v0 and v2-v0. With these functions, we can easily generate the surface for the horn like this:

require("surface_generator")

function horn(n)
    assert(n >= 8)

    -- surface function
    local function sfunc(u, v)
        local cosv = 2 + u*math.cos(v)
        local p = 2*u*math.pi
        return cosv*math.sin(p),
               cosv*math.cos(p) + 2*u,
               u*math.sin(v)
    end

    -- normal function
    local function nfunc(u, v)
        local cosu, sinu, cosv, sinv =
              math.cos(2*u*math.pi), math.sin(2*u*math.pi),
              math.cos(v), math.sin(v)
        local p = 2 + u*math.cos(v)
        local q = 2*math.pi
        return zeMake.normal2(0, 0, 0,
                            -u*sinv*sinu,
                            -u*sinv*cosu,
                             u*cosv,
                             cosv*sinu + p*q*cosu,
                             cosv*cosu - p*q*sinu + 2,
                             sinv)
    end

    local U, V = zeUtl.new("double", "double")
    U:range(0, 1/n, n+1)
    V:range(0, math.pi/n, 2*n+1)
    
    return surface_generator(U, V, sfunc),
           surface_generator(U, V, nfunc)
end

If the normal calculated with zeMake.normal2() points to the opposite side of the surface that you intend to light up, changing the input sequence will produce the correct results most of the time. Here is the code for testing the function:

    require("plot_simple")
    plot = plot_simple.new()
    plot:add_static(zeGrf.new("light"))
    require("horn")
    xyz, nor = horn(32)
    xyz:scale(50, 50, 50)
    shape = zeGrf.new("polygon")
    shape:set{type = "quads", vertex = xyz,
              vertex_normal = nor, color = {0, .7, .7, 1}}
    plot:add(shape)
    plot:animate()

2. Figure-8 Klein Bottle

For some complex shapes, not all generated normals point outward even if you alternate the input sequence of the zeMake.normal2() function. A trick to solve the problem is to change the u and v ranges to find out the range that produces inward normals; and then modify the normal function accordingly, as shown in the Figure-8 Klein bottle function:

require("surface_generator")

function klein8_bottle(n, a)
    assert(n > 16)
    assert(a > 0)

    local function cfunc(u, v)
        local cosu, sinu, cos2u, sin2u, sinv, sin2v =
              math.cos(u), math.sin(u),
              math.cos(u/2), math.sin(u/2),
              math.sin(v), math.sin(2*v)
        local r = a + sinv*cos2u - sin2v*sin2u/2
        return cosu*r,
               sinu*r,
               sin2u*sinv + cos2u*sin2v/2
    end

    local function nfunc(u, v)
        local cosu, sinu, cos2u, sin2u,
              cosv, sinv, cos2v, sin2v =
              math.cos(u), math.sin(u), math.cos(u/2), math.sin(u/2),
              math.cos(v), math.sin(v), math.cos(2*v), math.sin(2*v)
        local r = a + sinv*cos2u - sin2v*sin2u/2
        local ru = -sinv*sin2u/2 - sin2v*cos2u/4
        local rv = cosv*cos2u - cos2v*sin2u
        if v < 0 then
            return zeMake.normal2(0, 0, 0,
                  -sinu*r + cosu*ru,
                   cosu*r + sinu*ru,
                   cos2u*sinv/2 - sin2u*sin2v/4,
                   cosu*rv,
                   sinu*rv,
                   sin2u*cosv + cos2u*cos2v)
        else
            return zeMake.normal2(0, 0, 0,
                   cosu*rv,
                   sinu*rv,
                   sin2u*cosv + cos2u*cos2v,
                  -sinu*r + cosu*ru,
                   cosu*r + sinu*ru,
                   cos2u*sinv/2 - sin2u*sin2v/4)
        end
    end

    local U, V = zeUtl.new("double", "double")
    U:range(-math.pi, math.pi/n, 2*n+1)
    V:range(-math.pi, math.pi/n - 1.e-12, n+1)
    
    local xyz = surface_generator(U, V, cfunc)
    local nor = surface_generator(U, V, nfunc)

    V:range(0, math.pi/n, n+1)
    
    local xyz2 = surface_generator(U, V, cfunc)
    local nor2 = surface_generator(U, V, nfunc)
    n = xyz2:size()

    for k = 0, n-1 do
        local x, y, z = xyz2:get(k)
        xyz:add(x, y, z)
        x, y, z = nor2:get(k)
        nor:add(x, y, z)
    end

    return xyz, nor
end

3. Supper Surface

In some cases, it is very difficult to deduce the derivative equations, as for these super surface equations:

A simple solution is to estimate the derivative using the surface equtions with small steps of the independent variables. Here is an implementation:

require("surface_generator")

function super_surfaces(n, a, b, m, n1, n2, n3)
    assert(n > 16)
    assert(a ~= 0)
    assert(b ~= 0)
    assert(m > 0)
    assert(n1 > 0)
    assert(n2 > 0)
    assert(n3 > 0)

    local function radius(v)
        return 1 / math.pow(math.pow(math.abs(a*math.cos(m*v/4)), n1) + 
                            math.pow(math.abs(b*math.sin(m*v/4)), n2), n3)
    end
    
    local function sfunc(u, v)
        local ru, rv = radius(u), radius(v)
        return rv*math.cos(v)*ru*math.cos(u),
               rv*math.sin(v)*ru*math.cos(u),
               ru*math.sin(u)
    end

    local function nfunc(u, v)
        local d = 1.e-5
        local x, y, z = sfunc(u, v)
        local xu, yu, zu, xv, yv, zv
        if math.abs(u-math.pi/2) > d then
            xu, yu, zu = sfunc(u+d, v)
            xu, yu, zu = xu-x, yu-y, zu-z
        else
            xu, yu, zu = sfunc(u-d, v)
            xu, yu, zu = x-xu, y-yu, z-zu
        end
        if math.abs(v-math.pi) > d then
            xv, yv, zv = sfunc(u, v+d)
            xv, yv, zv = xv-x, yv-y, zv-z
        else
            xv, yv, zv = sfunc(u, v-d)
            xv, yv, zv = x-xv, y-yv, z-zv
        end
        return zeMake.normal2(0, 0, 0, xu, yu, zu, xv, yv, zv)
    end

    local U, V = zeUtl.new("double", "double")
    U:range(-math.pi/2, math.pi/n, n+1)
    V:range(-math.pi, math.pi/n, 2*n+1)
    
    local xyz = surface_generator(U, V, sfunc)
    require("check_normal")
    local nor = check_normal(xyz, surface_generator(U, V, nfunc))

    return xyz, nor

When the origin is inside an enclosed object, the normal should always point outward or have the same sign as the verex; then you can use the check_normal() function to ensure that.