LSL Scripting — Day, Night, and the In-Between

This is the fourth in a series of Caledon Oxbridge articles on scripting and the first based on an example script to explore. The third article was on script structure.

Several years back, I created a spherical residence up in the sky. It was large enough to have four floors. The bottom floor was a small lecture hall, next up was a floor for photo backdrops, then the main floor with tables and chairs, and finally, under the dome of the sphere, a floor for dancing. Both the sphere and the sim it was in now only exist in the mists of the past, but my treatment of the outside of the sphere makes for a nice example script. Enough to contain different parts of the script structure I discussed in the last article, but not so much as to drive anyone into a coma; which can be a tricky balance.

I wanted the appearance of the sphere to be unobtrusive, rather the color of the sky in both night and day. That meant that the coloring had to be chameleon-like in adapting to night and day, and thus we have a script.

First a word about colors in LSL. A color in LSL is a vector, so three floats taken as a unit as discussed in the second article in this series. Rather than the [0,255] closed range used in the inworld editor, scripts use such values divided by 255.0 yielding [0.0, 1.0] as the range of each color component.One caution if you are doing the dividing in LSL An integer divided by an integer truncates to an integer. To avoid that, at least one of the two numbers must be a float, otherwise the only color components you’ll get are zero and one. Notice that I wrote 255.0 above, not just 255.

If we are going to change the color of the outside surface of something, then we need to define what face we are changing. For standard prims, we can look that up in the Face Document. For mesh “prims”, we have to determine it by scripting (a later example). For a  sphere without any path cuts, the outside face is face 0.

For the script to change the color of the surface between night and day, it will need to check what’s current on some time interval. We don’t need to know yet what gets checked to be able to write our global variables. While they don’t strictly have to be global, making them so places them where they are easy to find and change. Also, using the all-caps plus underscores convention for constants is voluntary. It is not something that the language enforces. LSL, however, is case sensitive: agrajag is not Agrajag is not AgraJag when coding LSL. All of this brings us to:

// *** Global Variables ***

integer ACTIVE_FACE = 0;                   // The face on which to change the color
float   INTERVAL    = 180.0;               // Time between updates (seconds)
vector  COLOR_DAY   = <0.8, 0.9255, 1.0>;  // The color for daytime (pale blue-green)
vector  COLOR_NITE  = <0.1, 0.1, 0.1>;     // The color for night

As a reminder, a float, defined earlier in the series under variable types, is a number with a decimal point that can have a fractional part. Later, we will need to write a function to change the color on the desired face, but for the moment we can just write an empty stub.

// *** User Defined Functions ***

uuSetColor () {
}

Now, what is required in every LSL script? It’s not global variables and it’s not user defined functions. If you thought, “a default state”, you’ve got it down. With that out of the way, what do we want to do within that state? What event-handlers do we need?

When we start out, it would be nice to initialize the color. We also need to set a timer if we ever want to update the color again. Then, we’ll need a timer event-handler to update the color whenever the timer goes off. Timers in LSL, once set, reset themselves and keep producing timer events until reset with a zero interval. So the main part of the script looks like this:

// *** Event-Handler Containing States ***

default {
    
    state_entry () {
        uuSetColor ();
        llSetTimerEvent (INTERVAL);
    }
        
    timer () {
        uuSetColor ();
    }
}

Well, that’s not too complicated. Set the color and a timer on state_entry (entering the default state), then update the color whenever the timer goes off. We need INTERVAL to set the timer, but we defined that and initialized it way back in the global variables section. Now we just have to figure out what to do in our user defined function.

As long as the sim we are on is using the standard SL day/night cycle, we could get the approximate direction toward the sun. That comes as a normalized (i.e. having length of one)  X,Y,Z vector, and the normalization makes each of the three elements into a direction cosine Since this is a vector with three direction components, we can grab the z-component. The documentation says that the sun is above the horizon if the z-component is greater than or equal to zero.. If your geometry isn’t in place, you’ll have to take my word for it that this implies that the angle is measured from the zenith (straight-up) and the cosine varies between 1 to -1 as the angle goes from the zenith to the nadir (straight-down), becoming zero at the horizon. The final consideration is an implementation detail.

We don’t really want to interpolate the color (i.e. smoothly calculate intermediate colors) of the surface over the entire possible range of the cosine. If we trim the range to interpolate from 30 degrees above the horizon to 30 degrees below the horizon it will work nicely. And getting that range means that we trim the range of the z-component to 0.5 to -0.5. Now we can write our user function, put it all together, and add a few comments to the top so that I know what it is and when I wrote it six months from now and others will know whom to blame. 😛

// ********************************************************************
//  Color4Sun
//      Interpolate between two colors based on the sun's 
//      angle from the zenith.
//
//  Wordsmith Jarvinen - 25 August 2018
//
// ********************************************************************

// *** Global Variables ***

integer ACTIVE_FACE = 0;                   // The face on which to change the color
float   INTERVAL    = 180.0;               // Time between updates (seconds)
vector  COLOR_DAY   = <0.8, 0.9255, 1.0>;  // The color for daytime
vector  COLOR_NITE  = <0.1, 0.1, 0.1>;     // The color for night

// *** User Defined Functions ***

uuSetColor () {

    // Variables declared within this function are local to the function.
    // They are released when the function returns to the calling code.

    // The sun directions is a normalized vector pointing approximately toward the sun,
    // hence its elements are direction cosines. The Z component is 1 to -1 referenced 
    // to the zenith, making it greater than zero when the sun is above the horizon.
    
    vector sundir = llGetSunDirection();
    float  zcos   = sundir.z;
    
    // We don't need to interpolate over the entire range. Clipping the values
    // below interpolates only when the sun is within 30 degrees of the horizon
    if ( zcos >  0.5 ) { 
        zcos =  0.5;
    } else if ( zcos < -0.5 ) {
        zcos = -0.5;
    }
    
    // We can interpolate the color as a vector, we don't have to do the 
    // RGB components separately 
    vector colr = (0.5+zcos)*COLOR_DAY + (0.5-zcos)*COLOR_NITE;
    llSetColor (colr,   ACTIVE_FACE);
}

// *** Event-Handler Containing States ***

default {
    
    state_entry () {
        uuSetColor ();
        llSetTimerEvent (INTERVAL);
    }
        
    timer () {
        uuSetColor ();
    }
}

That finishes our first example script. Once again thank you for your attention. In the next article, we get a bit windy.

Caledon Oxbridge also has an inworld Basic Scripting class, Mondays at noon SLT in our main lecture hall. Please see our schedule page.

Leave a Reply

Your email address will not be published. Required fields are marked *