--*************************************************************************** --*************************************************************************** -- 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