
| Lua OpenGL Surface Iso-surface 3D plot Sub-plots |
Surface GeneratorThe 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. HornThe 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 BottleFor 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 SurfaceIn 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. |