this script features a short voyage over Mars' surface.
Two remarks:
1) A B-Spline is used to move the viewer and his viewing direction separately, with the result of a quite fluid movement.
2) It is extremely easy to create new "journeys", because of the way paths are defined:
- *) Path points are selected by placing the cursor over an interesting texture map's point (a crater, for example) and reading its coordinates as displayed by a simple image editor.
*) The script automatically determines how long the user will stay above a certain place, based on its associated description text's length.
This means that no cel/celx programming skills are needed to create simple but enjoyable planetary excursions.
You need to copy the code below and paste it on your text editor. Then save it as anyname.celx in your current Celestia folder.
(In order to run it, you will need a recent 1.3.2 prerelease)
Code: Select all
--***************************************************************************
--***************************************************************************
-- An non-uniform B-Spline approximation algorithm
-- Applied to Mars touring
--
-- Coded by Toti
--
--***************************************************************************
--***************************************************************************
--***************************************************************************
-- Constants
--***************************************************************************
DELAY = 5
SEC_LETTER = 0.1 -- user takes this time to read a single letter (increase if you find the text's
-- speed too fast)
KM_MLY = 9466411.842
KM_AU = 149597870.7
BODY = "Sol/Mars"
ZERO = celestia:newposition (0, 0, 0)
OBS = celestia:getobserver()
ORDER = 3 -- order of the curve. Increase for additional smoothness
IM_WIDTH = 1024 -- texture dimensions used as input for the Control Points
IM_HEIGHT = 512
-- control points, given as:
-- 1] flight path (x,y) as pixel positions in BODY's texture map; z as altitude (a BODY's radius multiplier)
-- 2] lookat positions (x,y) as pixel positions in BODY's texture map; z as altitude (a BODY's radius multiplier)
-- 3] info field (with descriptive text):
FEATURES = { { 180, 50, 15, 134, 205, 1, "Here we are, enjoying an impressive view above the fourth planet from Sol:"},
{ 180, 50, 12, 134, 205, 1, "Mars, named after the Roman god of war.\nLet's head towards Tharsis Regio..."},
{ 0, 150, 3, 134, 205, 1, "...a very conspicuous highland, full of outstanding features."},
{ 90, 220, 1.1, 131, 204, 1, "Now we are over Olympus Mons, a huge shield volcano, probably the largest one in the Solar System."},
{141, 236, 1.1, 131, 204, 1, "Its summit is 24 km. above the surrounding plains."},
{163, 180, 1.1, 131, 204, 1, "Note the deep scarp (up to 6 km. high) that frames it."},
{163, 180, 1.1, 131, 204, 1, "There are several other volcanoes with a similar structure:..."},
{247, 148, 1.2, 214, 224, 1, "...very prominent is this almost colinear set: Ascraeus,..."},
{174, 250, 1.3, 193, 254, 1, "...Pavonis,..."},
{165, 298, 1.2, 169, 283, 1, "...and Arsia."},
{165, 298, 1.5, 169, 283, 1, "Each one of this three shield volcanoes is about 15 km. above the nearby area."},
{172, 285, 1.2, 200, 275, 1.01, "The intense tensions associated with the Tharsis bulge formation cracked the lithosphere,..."},
{185, 275, 1.1, 205, 275, 1.01, "...creating these intrincated patterns that we are flying over, which received the name of Noctis Labyrinthus."},
{238, 274, 1.05, 255, 274, 1.01, "Now we are entering Valles Marineris, a system of canyons about 4000 km. long..."},
{292, 276, 1.05, 310, 286, 1.01, "...probably originated by a mixture of collapses and fluvial sculpture. Its walls reach 10 km. high."},
{340, 294, 1.05, 360, 300, 1.01, "Note that the overall structure is quite complex, with various crests and parallel channels."},
{355, 294, 1.05, 360, 300, 1.01, "Let's climb a bit so we can have the whole picture."},
{365, 294, 1.8, 300, 274, 1, "A quite impressive postcard, indeed."},
{365, 294, 1.8, 270, 274, 1, "Now let's change direction, let's fly towards South..."},
{361, 302, 1.8, 361, 320, 1.01, "...which is heavily cratered, and full of smaller features."},
{365, 375, 1.75, 391, 400, 1, "We are reaching Argyre Planitia, a highly preserved impact basin."},
{412, 398, 1.75, 391, 400, 1, "Note the rugged patterns. Specially interesting are the radial ones..."},
{412, 398, 1.1, 375, 381, 1, "...here..."},
{412, 398, 1.1, 375, 381, 1, "...(Probably a drainage of the hypothetical lake that once filled the basin)..."},
{412, 398, 1.1, 412, 414, 1, "...and here."},
{391, 400, 1.1, 412, 414, 1, "This formation probably conducted flows into this basin's lake."},
{400, 415, 1.1, 412, 414, 1, "We are near the South Pole. Let's move closer to it."},
{330, 470, 1.01, 256, 510, 1, "The ice formations here are notably smaller than the North Pole ones."},
{270, 475, 1.05, 256, 510, 1, "This cap is composed of water ice and carbon dioxide (dry) ice..."},
{210, 480, 1.1, 256, 510, 1, "...arranged in a multilayered disposition with dark dust bands in between."},
{150, 485, 1.1, 256, 510, 1, "Both the northern and southern Mars poles dramatically recede on a seasonal basis."},
{ 90, 490, 1.1, 256, 510, 1, "As temperature increases, the carbon dioxide sublimates..."},
{ 30, 495, 1.1, 256, 510, 1, "...ie. it passes from solid to gaseous state without getting liquid."},
{992, 500, 1.1, 256, 510, 1, "This huge gaseous mass produces substantial changes in martian atmospheric pressure."},
{932, 500, 1.1, 256, 510, 1, "In addition to these annual oscillations, there is also evidence that this cap is slowly dwindling..."},
{872, 500, 2, 256, 510, 1, "...possibly due to a progressive climate change."},
{791, 408, 1.75, 700, 375, 1, "The Hellas Planitia was the result of a giant asteroid impact."},
{732, 305, 2, 700, 375, 1, "Extending 2300 km. across, the central basin is about nine kilometers deep (the deepest point on Mars)."},
{603, 356, 2.5, 700, 375, 1, "A ring of material (almost surely thrown out by the impact itself)..."},
{705, 428, 3, 700, 375, 1, "...covers all this almost circular surface up to a diameter of nearly 4000 km."},
{705, 428, 3.2, 700, 375, 1, "Finally, we will move to Elysium Planitia, the second largest volcanic zone of the planet..."},
{897, 351, 4, 940, 194, 1, "...quite far from our previous position."},
{952, 237, 1.1, 940, 194, 1, "There are three massive volcanoes here:"},
{910, 208, 1.1, 940, 202, 1, "Albor,... "},
{960, 195, 1.1, 935, 190, 1.03, "...Elysium (the largest one in the area),... "},
{929, 158, 1.1, 945, 164, 1, "...and Hecates. "},
{977, 143, 1.2, 913, 173, 1, "This plain is really a huge dome of about 2400 by 1700 km..."},
{997, 180, 1.5, 913, 173, 1, "...crossed by several radial fractures that evidence crust stress during bulge development."},
{999, 200, 1.8, 913, 173, 1, "Those cracks are specially noticeable in this region's western slopes..."},
{973, 215, 2, 913, 173, 1, "The rest of the place is notably smooth with the exception of some wrinkles and a few craters: an even terrain..."},
{953, 233, 2.2, 913, 173, 1, "...like the already visited Tharsis."},
{600, 230, 3.4, 700, 190, 1, "This concludes our very brief voyage."},
{398, 255, 6, 134, 205, 1, "I hope it was interesting."} }
-- *********************************************************
-- Touring functions
-- *********************************************************
function buildControlPts (L, bRad, oAmb)
-- builds a table of UCS control points where the flightpath & 'look at' positions will pass through
local size = table.getn (L)
local P = {}
local i
for i = 1, size do
-- adapt to Celestia's Prime Meridian conventions:
local long = ( L[i][1] / IM_WIDTH * 360 ) - 180
local lat = 90 - ( L[i][2] / IM_HEIGHT * 180 )
local r = bRad * L[i][3]
local pos = sphe2cart (r, long, lat)
long = ( L[i][4] / IM_WIDTH * 360 ) - 180
lat = 90 - ( L[i][5] / IM_HEIGHT * 180 )
r = bRad * L[i][6]
local view = sphe2cart (r, long, lat)
-- include ambient light curve directly in the BSpline (this is done so all features are shown, including those
-- in the planet's dark side:
if i==1 or i==size then
P[i] = {pos.x, pos.y, pos.z, view.x, view.y, view.z, i, oAmb}
else
P[i] = {pos.x, pos.y, pos.z, view.x, view.y, view.z, i, 1}
end
end
-- add a blank point so there is room at the end of the fitting scheme (so next-to-last point shows up):
L[size+1] = {L[size][1], L[size][2], L[size][3], L[size][4], L[size][5], L[size][6], ""}
P[size+1] = {P[size][1], P[size][2], P[size][3], P[size][4], P[size][5], P[size][6], size+1, oAmb}
return P
end
-- *********************************************************
-- B-Spline functions
-- *********************************************************
function getDuration (L, size)
-- returns total time of the tour, based on info field in table L
local j
local length = 0
for j = 1, size do
length = length + string.len ( L[j][7] )
end
return (length * SEC_LETTER)
end
function buildKnots (L, ord, size, dur)
-- builds a table of non-uniform knots as a function of L info field length
local j
local K = {}
local kn = size + ord
local plength = 0
for j = 1, kn do
if j <= ord then
K[j] = 0
elseif j > size then
K[j] = dur+1
else
plength = plength + string.len( L[j-ord][7] )
K[j] = plength * SEC_LETTER
end
end
return K
end
function N (K, i, ord, curr)
-- returns the blending factor for 'curr' position along the curve, using 'K' as knot vector. Uses recursion
local blend
if ord == 1 then -- definition 1st case
if K[i] <= curr and curr < K[i+1] then
blend = 1
else
blend = 0
end
else
local k1 = i+ord-1
local k2 = i+ord
if K[k1] == K[i] and K[k2] == K[i+1] then -- be careful with 'division by zero' errors at the extremes
blend = 0
elseif K[k1] == K[i] then
blend = (K[k2] - curr) / (K[k2] - K[i+1]) * N(K, i+1, ord-1, curr)
elseif K[k2] == K[i+1] then
blend = (curr - K[i]) / (K[k1] - K[i]) * N(K, i, ord-1, curr)
else -- definition 2nd case
blend = (curr - K[i]) / (K[k1] - K[i]) * N(K, i, ord-1, curr) + (K[k2] - curr) / (K[k2] - K[i+1]) * N(K, i+1, ord-1, curr)
end
end
return blend
end
function buildSpline (K, CP, rows, cols, ord, curr)
-- returns the B-Spline value for 'curr' knot position
local i, j
local S = {}
for j = 1, cols do
S[j] = 0
end
for i = 1, rows do
local w = N(K, i, ord, curr)
for j = 1, cols do
S[j] = CP[i][j] * w + S[j]
end
end
return S
end
--***************************************************************************
-- Other functions
--***************************************************************************
function sphe2cart (rad, rho, phi)
-- converts from spherical to cartesian coordinates. Returns a POSITION
local r = rad / KM_MLY
local x = - r * math.cos (math.rad (rho)) * math.cos (math.rad (phi))
local y = r * math.sin (math.rad (phi))
local z = r * math.sin (math.rad (rho)) * math.cos (math.rad (phi))
return celestia:newposition (x,y,z)
end
function celestia_cleanup_callback()
-- restores user's preferred settings
celestia:setambient (oldAmb)
end
--***************************************************************************
-- Main routine
--***************************************************************************
oldAmb = celestia:getambient()
body = celestia:find(BODY)
bodyRad = body:radius()
bodyFrame = celestia:newframe("planetographic", body)
TOUR = buildControlPts (FEATURES, bodyRad, oldAmb)
celestia:select (body)
tableSize = table.getn (TOUR)
datumSize = table.getn (TOUR[1])
duration = getDuration (FEATURES, tableSize)
Knots = buildKnots (FEATURES, ORDER, tableSize, duration)
t0 = celestia:getscripttime()
t1 = 0
repeat
pos = buildSpline (Knots, TOUR, tableSize, datumSize, ORDER, t1)
obsPos = bodyFrame:from (celestia:newposition (pos[1], pos[2], pos[3])) -- body is rotating, so UCS won't work OK
lookAtPos = bodyFrame:from (celestia:newposition (pos[4], pos[5], pos[6])) -- same here
normalVect = obsPos-body:getposition()
OBS:setposition (obsPos)
OBS:lookat (lookAtPos, normalVect)
celestia:setambient (pos[8])
celestia:flash (string.format("%s", FEATURES[math.floor(pos[7])][7]), DELAY)
wait()
t1 = celestia:getscripttime() - t0 -- put this here so old computers (like mine) can run the code fine
until t1 > duration