Showing posts with label lsl. Show all posts
Showing posts with label lsl. Show all posts

CEF, New Media, and Old Videos

Recently (June 23rd), the Linden viewer released with an important update to "CEF" - the Chromium Embedded Framework that's used for viewing web pages and -- crucially -- media. At the time of writing, only the Linden viewer has this support, but third party viewers will likely adopt this feature very soon.

Support for the new CEF version first surfaced in a project viewer that used streaming media for the Adult Swim "The Shivering Truth" event, in May.

But there are broader benefits of the enhanced media support. Notably, other video services and formats are now supported that before would give a blank screen (at best). One such service is Vimeo, fairly widely used in entertainment and, specific to SL, the home of an exhaustive archive of old Designing Worlds videos that were once hosted by Treet TV.

Unlocking these Vimeo-based archives needs a bit of tweaking on in-world video players. It's actually possible to play these videos on either "Shared Media" surfaces (aka "Media on a Prim") or Parcel Media. Both will use a custom URL that embeds Vimeo's proprietary streaming player to see the video directly instead of a web page surrounding the player. The basic player URL is offered as a way to "Share" the video from the regular Vimeo web page, but there are a lot of confusing options. What I've found to work best is to copy the number in the Vimeo webpage URL (call that VIMEO_NUMBER), and splice it into an address as follows:
https://player.vimeo.com/video/VIMEO_NUMBER?autoplay=1&loop=1
which sets it to autoplay and loop by default. (Note the "player" in that address.)

Both Shared and Parcel media also face a quirk of the Vimeo player: it displays the video in the center 56.25% of the vertical surface texture - a huge "letterbox" effect. To get around this, a script can scale the texture to stretch out that strip to span the whole height of the surface, or you can do this manually in the editor. (To get a black letterbox, include "&transparent=0" in the URL.)

When presented in Shared Media, the user can interact directly with the viewer by clicking on it (or using the keyboard with the surface selected), so you'll likely want to turn off the Shared Media control options that hover above the display. Unlike Shared Media, Parcel media has no means of interacting directly with the viewer, so using "&controls=0" will hide the Vimeo player scrollbars, etc. (Some Parcel media players have their own controls, *some* of which will operate the Vimeo player stream.)

Here's a little script that does what's described here for both Shared and Parcel media, using a couple different Designing Worlds episodes of personal interest.

==================
Vimeo Media Script
==================

string VIMEO_NUMBER = "427716338";
integer MEDIA_FACE = 1;
mediaError(integer num)
{
list MEDIA_ERRORS =
[ STATUS_MALFORMED_PARAMS, "Function was called with malformed parameters"
, STATUS_TYPE_MISMATCH, "Argument(s) passed to function had a type mismatch"
, STATUS_BOUNDS_ERROR, "Argument(s) passed to function had a bounds error"
, STATUS_NOT_FOUND, "Object or other item was not found"
, STATUS_NOT_SUPPORTED, "Feature not supported"
, STATUS_INTERNAL_ERROR, "An internal error occurred"
, STATUS_WHITELIST_FAILED, "Whitelist Failed"
];
llWhisper(DEBUG_CHANNEL, "Shared media error encountered: "
+ llList2String(MEDIA_ERRORS, llListFindList(MEDIA_ERRORS, [num])+1));
}
default
{
state_entry()
{
integer errNum = llClearLinkMedia(LINK_THIS, MEDIA_FACE);
if (errNum)
{
mediaError(errNum);
return;
}
string html = "https://player.vimeo.com/video/"
+ VIMEO_NUMBER
+ "?autoplay=1&loop=1";?
llScaleTexture(1.0, 0.5625, MEDIA_FACE);
errNum = llSetLinkMedia(LINK_THIS, MEDIA_FACE,
[ PRIM_MEDIA_AUTO_PLAY, TRUE
, PRIM_MEDIA_CURRENT_URL, html
, PRIM_MEDIA_HOME_URL, html
, PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE
]);
if (errNum)
mediaError(errNum);
// parcel
// return; // to prevent setting PARCEL media
html = "https://player.vimeo.com/video/"
+ "375405889"
+ "?autoplay=1&loop=1&controls=0&transparent=0";
llParcelMediaCommandList([ PARCEL_MEDIA_COMMAND_STOP]);
llParcelMediaCommandList([ PARCEL_MEDIA_COMMAND_UNLOAD]);
llParcelMediaCommandList(
[ PARCEL_MEDIA_COMMAND_URL, html
, PARCEL_MEDIA_COMMAND_TYPE, "text/html"
]);
llParcelMediaCommandList([ PARCEL_MEDIA_COMMAND_PLAY]);
}
}


==================

All the scripted operations can be done manually, too, and may work in existing in-world media players. Note that a parcel media script must be owned by the landowner, so on group owned land it must be deeded to the group. And remember that you'll need a viewer supporting the CEF updates to actually watch the videos.
Reporter Qie Niangao
20200708

Sit a spell. Or don't

A recent scripting forum post asked for help with Second Life objects we sit on, and listed examples: a prize-awarding chair, a dance pole, and a teleporter. That list reminded me how often in SL we use a "sit" interaction to trigger some wholly unrelated event -- nothing about conventional "sitting" at all -- because scripts get these handy artefacts of the avatar being on a scripted seat assembly.

Teleporting, for example. Scripters have several variants of "sit teleporters" but anybody new to SL must wonder why ever they should "sit" on a thing in order to be transported somewhere else. After a while we get so used to it that we worry that, instead, walk-thru (Experience) portals might "break immersion" -- as if being transported by sitting is somehow more intuitive. We can disguise the "sittingness" of the interaction with llSetSitText() so the right-click menu shows something like "Teleport" instead of "Sit here" but still it involves an explicit right-click gesture. Or, we could make the teleport trigger with a left click with a llSetClickAction of _SIT, but that makes a mouse cursor of a chair -- hardly universal symbol for teleportation!

I have no idea why a prize chair should be a chair. Somebody scripted one that way once, I guess. Maybe it gives a hint of exclusion to others who'd want to claim the prize? Because the sitter is occupying the space while the prize is distributed, so nobody else confuses the process? From the prizewinner's point of view, though, the chair is just weird. Analogue of a throne for coronation? Seems a stretch.

Even though dancing isn't much like sitting, the dance pole is the LEAST artificial of the examples. The real reason to "sit" on a dancepole is to lock the dance animation to the location of a static prop (the pole). Again, using "Dance!" as the sit text makes it a little more intuitive, but a chair -ClickAction mouse cursor is no more evocative of poledancing than of teleporting.

Builders and scripts - particularly those developing with an Experience target - might consider adopting, whenever possible, some more natural interaction, even if it needs to secretly trigger a forced, scripted "sit" behind the scenes, and reserve explicit "sitting" for those special occasions when the user is literally choosing a place to play a real sit animation.
Reporter Qie Niangao
20200605

Geek To The EEP

In an outdoor photo, you can see the weather. In a film, you can FEEL the weather. That's because the dynamic medium represents how the weather CHANGES environmental cues: Clouds move in and the sky darkens, lighting changes, the sea gets heavy.

What if we could give our visitors that feeling in an intentional, _directed_ series of environmental settings? Immerse them in a little program of weather dynamics?

Below is a sample Experience script that demonstrates features of the new Environment Enhancement Project (EEP), to provide just that kind of weather-changing experience, individually for each visitor. As configured, it applies a sequence of four Library environmental "sky" assets to make the weather seem progressively more threatening over about a minute and a half after arrival. (In real use, you might choose longer transition intervals to stretch this out more realistically. And of course you can use other EEP environments in any sequence - perhaps a cheery brightening of the sky as clouds lift.)


/*
Released into Public Domain, 2020, Qie Niangao for Bay City Post
Give visitors an enhanced experience with a SERIES of Environments
So clouds can build, sky can darken, seas roil more over time - or vice versa
(sample Environments in ENV_STAGES below are super primitive, just to demo the concept)
This basic script uses simple passage of time to trigger next stage
Make sure parcel (at least) has the script's Experience enabled
*/

//== USER SPECIFIED CONSTANTS =======================

integer AGENT_SCOPE = AGENT_LIST_PARCEL; // or AGENT_LIST_PARCEL_OWNER or AGENT_LIST_REGION
// but if they're on land without the Experience, there will be errors
list ENV_STAGES = // sequence of (SAMPLE) Environment UUIDs and durations (in seconds)
[ "d15d3fb0-7e14-4af8-203e-33b85346f3f1", 20 // "Neutral" Library sky
, "cd880f86-53c7-1a91-532c-2797f517a35f", 20 // "Daytime shadows" Library sky
, "07899451-b3d8-9f5c-563b-14b89eda481d", 30 // "Dusty" Library sky
, "2a000d12-e692-da2a-9e08-0cbda70bc0bf", 30 // "PaperSnow" Library sky
]; // These use unnaturally short durations for demo & testing)
float AVATAR_SCAN_INTERVAL = 10.0; // in seconds.
// Not a real sensor, but still some list manipulation, so keep it as long as tolerable
// A new arrival may wait UP TO this interval, so just half this time on average
integer MAX_IGNORED_AGENTS = 20; // after which, forget the oldest (prevent memory leak)
integer MAX_STARTING_AGENTS = 3; // most we'll wait to reply to permissions request (prevent leak)

//== SCRIPT-MANAGED VARIABLES =======================

list tAvStages; // avatar UUIDs in scope, index of their Env.stage, preceded by time to move through stage
list ignoredAgents; // don't spam these with more exp perm requests
key parcelID;
list agentsStarting;

debugOut(string outStr) {
// llOwnerSay(outStr);
}

default
{
state_entry()
{
if ([] == llGetExperienceDetails(NULL_KEY))
{
llWhisper(DEBUG_CHANNEL, "Script \""+llGetScriptName()+"\" is not associated with an experience. Exiting.");
return;
}
// trivial, impossible to match sensor, just set a repeating no_sensor interval:
llSensorRepeat("NO SUCH NAME", llGetKey(), AGENT, 0.01, 0.01, AVATAR_SCAN_INTERVAL);
parcelID = llList2Key(llGetParcelDetails(llGetPos(), [PARCEL_DETAILS_ID]), 0);
}
on_rez(integer start_param)
{
llResetScript();
}
no_sensor()
{
list newAgents = llGetAgentList(AGENT_SCOPE, []);
// any already-staged agents need removing?
integer oldAgentIdx = llGetListLength(tAvStages) - 2;
while (0 <= oldAgentIdx)
{
key oldAgent = llList2Key(tAvStages, oldAgentIdx);
if (-1 == llListFindList(newAgents, [oldAgent]))
tAvStages = llDeleteSubList(tAvStages, oldAgentIdx-1, oldAgentIdx+1);
oldAgentIdx -= 3;
}
// any newly-arrived agents need request to add?
integer newAgentIdx = llGetListLength(newAgents);
while (0 <= --newAgentIdx)
{
key newAgent = llList2Key(newAgents, newAgentIdx);
integer scopeIdx = llListFindList(tAvStages, [newAgent]);
if (-1 == scopeIdx) // agent not yet in list of agents with managed environments
if ((-1 == llListFindList(ignoredAgents, [newAgent])) // agent didn't deny us in past
&& (-1 == llListFindList(agentsStarting, [newAgent]))) // and not already being invited
{
if (!llAgentInExperience(newAgent)) // agent new to this Experience: greet & invite
llRegionSayTo(newAgent, 0, "Greetings "+llGetDisplayName(newAgent)
+" and welcome! You're being invited to an Experience, \""
+ llList2String(llGetExperienceDetails(NULL_KEY), 0) // script's Experience name
+"\" that demonstrates dynamic personal Environment settings."
+" We hope you'll agree to participate."
+" (If you don't grant permissions, this script will try not to ask again for a while.)");
agentsStarting += newAgent;
if (MAX_STARTING_AGENTS < llGetListLength(agentsStarting))
agentsStarting = llList2List(agentsStarting, 1, -1);
llRequestExperiencePermissions(newAgent, "");
}
}
}
experience_permissions_denied(key agentId, integer reason)
{
integer agentStartingIdx = llListFindList(agentsStarting, [agentId]);
if (-1 != agentStartingIdx)
{
if (XP_ERROR_NOT_PERMITTED == reason)
{
if (-1 == llListFindList(ignoredAgents, [agentId]))
{
ignoredAgents += agentId; // don't spam after first request
if (llGetListLength(ignoredAgents) >= MAX_IGNORED_AGENTS)
ignoredAgents = llList2List(ignoredAgents, 1, MAX_IGNORED_AGENTS);
}
}
agentsStarting = llDeleteSubList(agentsStarting, agentStartingIdx, agentStartingIdx);
}
else
if (XP_ERROR_NOT_PERMITTED_LAND != reason)
// ignore _LAND perm error because it's raised every time agent moves to non-XP parcel
llWhisper(DEBUG_CHANNEL, "Experience permissions denied, reason #"+(string)reason
+": "+llGetExperienceErrorMessage(reason));
}
experience_permissions(key agentId)
{
integer agentStartingIdx = llListFindList(agentsStarting, [agentId]);
if (-1 != agentStartingIdx)
{
tAvStages = [llGetUnixTime(), agentId, 0] + tAvStages;
llSetTimerEvent(0.1);
agentsStarting = llDeleteSubList(agentsStarting, agentStartingIdx, agentStartingIdx);
return;
}
// else handle timer-triggered requests (process the tAvStages queue)
integer now = llGetUnixTime();
if (now >= llList2Integer(tAvStages, 0))
{
key agent = llList2Key(tAvStages, 1);
integer envIdx = llList2Integer(tAvStages, 2);
key env = llList2Key(ENV_STAGES, envIdx);
integer transition = llList2Integer(ENV_STAGES, envIdx + 1);
debugOut("process env "+(string)env+" for agent "+llKey2Name(agent)
+"\n\t with tAvStages = "+llDumpList2String(tAvStages, " | "));
integer envErr = llReplaceAgentEnvironment(agent, (float)transition, env);
if (0 > envErr)
llWhisper(DEBUG_CHANNEL, "Error in llReplaceAgentEnvironment : "+(string)envErr);
envIdx += 2; // advance agent to wait for next stage
integer nextTime = now + transition;
if (envIdx >= llGetListLength(ENV_STAGES))
nextTime = 2147483647; // Final env stage should last for rest of stay (MAXINT)
tAvStages = llListReplaceList(tAvStages, [nextTime, agent, envIdx], 0, 2);
}
tAvStages = llListSort(tAvStages, 3, TRUE); // lazy
float timeTo = (float)(llList2Integer(tAvStages, 0) - now);
if (0.0 >= timeTo) timeTo = 0.1;
llSetTimerEvent(timeTo);
}
timer()
{
llSetTimerEvent(0); // set again in exp_perms event
if (llGetListLength(tAvStages)) // any Avs left?
llRequestExperiencePermissions(llList2Key(tAvStages, 1), "");
}
}

Note that Experience permissions are required by llReplaceAgentEnvironment, the EEP function that makes this possible, so the script will need to be compiled to an Experience that's enabled on the land where it runs. And visitors have to grant the permissions requested when the script invites them to participate.

At the moment, EEP is only visible in the Linden viewer, but soon it'll be supported in popular third party viewers too. Although the script sequences the effect individually for each viewer, it manipulates what's called the "Shared Environment" -- so it can be overridden by users who specify their own environment or time-of-day.

Passage of time needn't be the only thing to trigger a change of EEP environment. Should one particular room or area have different lighting? Such a script would track where visitors roam and update their environments accordingly. Or maybe trigger environments by level in an in-world game, or in response to some scripted role-play interaction.

Another possible extension: _Sound_ is powerfully evocative of weather: wind, thunder, bird song, waves, etc: llPlaySound() in a HUD is heard only by the wearer; llAttachToAvatarTemp() could transparently attach a sound-emitting HUD to experience participants.
Reporter Qie Niangao
200504

Slow Inventory Saves a Step

Perhaps you, like me, find yourself copying the contents of one object into another, for example to move a whole bunch of animations from one piece of furniture into a new one. Out of habit, I've always done this by first copying everything into my own Inventory, then back out to the other object. This is really slow and prone to failure. Some third party viewers have a way to make it a little more reliable, but it's still pretty painful.

Thanks to particularly sluggish Inventory services I recently realized that this was a silly extra step: All I really need to do is copy the contents directly, object to object. So here's a script that does that (read the comments for some caveats and options about how it works):

/*
Simple little script to copy the contents of this prim to another, identified by key in textbox
To get the key to put in that textbox, can use Firestorm's "Copy Keys" button in the Build Tool editor or drop this trivial script into the target object:

default { state_entry() { llOwnerSay((string)llGetKey()); } }

Unfortunately (and weirdly) I don't know a way for a script to get the permissions on a rezzed object, so this will try to copy to anything the toucher owns, which won't work for no-mod targets. This expects both the sending and recipient objects to be owned by the toucher.

Some special cases might make sense with other owners, but lots of ways for that to cause trouble, too. Can theoretically use something like this to give contents to an avatar
but as written it would wait 2 seconds between items. Some TPVs have built-in means of doing this reliably.

Could use llGiveInventoryList() to make it faster, but that can't work at all for no-copy,
which would mean a hybrid approach, much like product unpacking scripts.
*/

integer COPY_SCRIPTS = FALSE;

// Scripts would arrive confusingly NOT running (use TRUE to copy them anyway, including THIS script,

// but then will need to either recompile them or take target object into inventory and re-rez it.)

integer MOVE_NO_COPY_CONTENTS = FALSE;

// It could damage the sender; use TRUE to do it anyway, but that won't work if copying to an attachment

integer listenHandle;



default

{

touch_start(integer num_detected)

{

key toucher = llDetectedKey(0);

if (llGetOwner() != toucher)

{

llRegionSayTo(toucher, 0, "You need to own this object to copy its contents");

return;

}

llListenRemove(listenHandle);

integer chan = -1000000000 - (integer)llFrand(1000000000);

llListen(chan, "", toucher, "");

llTextBox(toucher, "Key (UUID) of recipient object?", chan);

}

listen(integer channel, string name, key id, string text)

{

key target = (key)text;

if (NULL_KEY == target)

llRegionSayTo(id, 0, "Sorry, that's not a valid key");

else

if (id != llGetOwnerKey(target))

llRegionSayTo(id, 0, "Sorry, you don't own that object");

else

{

integer inv = llGetInventoryNumber(INVENTORY_ALL);

while (--inv >= 0)

{

string invName = llGetInventoryName(INVENTORY_ALL, inv);

if (COPY_SCRIPTS || (INVENTORY_SCRIPT != llGetInventoryType(invName)))

{

if (! (PERM_COPY & llGetInventoryPermMask(invName, MASK_OWNER)))

if (MOVE_NO_COPY_CONTENTS)

llGiveInventory(target, invName);

else

llRegionSayTo(id, 0, "Skipping no-copy contents: "+invName);

else

llGiveInventory(target, invName);

}

}

llRegionSayTo(id, 0, llGetScriptName()+": Done");

}

}

}
Reporter Qie Niangao
200302

A Hoverlogo And Other Sprites

Particles are two-dimensional images situated in 3-D space. They (except still-rare "ribbon" particles) have the special property of always facing the camera: they're "sprites" is the parlance of computer graphics. This is fine for many purposes, but in other situations can challenge immersion: there's no real world analogue of a surface that's always facing you regardless of how your view moves around it.

Well, there's the optical illusion of eyes in paintings such as the Mona Lisa that appear to follow as the viewer moves around the room, but particle sprites really do face the cam.

That's similar to hovertext which, too, is always drawn facing the cam; one big difference is that hovertext doesn't scale with distance from the emitter whereas particles do.

In SL, things that aren't realistic may break immersion -- or they may appear magical. Here we have what I prefer to see as magical: a "hoverlogo" that always shows the circular logo image head-on, regardless of whence it's viewed:

default
{
state_entry()
{
llParticleSystem(
[ PSYS_PART_FLAGS, 0
| PSYS_PART_EMISSIVE_MASK
| PSYS_PART_FOLLOW_SRC_MASK
, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_DROP
, PSYS_SRC_TEXTURE, "422ed2fa-482d-b9fd-1bd7-eef1611a1dbf"
, PSYS_PART_START_SCALE, <0.5, 0.5 , 0.0>
, PSYS_PART_MAX_AGE, 30.0
, PSYS_SRC_BURST_RATE, 3.0
]);
llSetText( "Visit the\nBay City Post", <1.0, 1.0, 1.0>, 1.0);
}

touch_start(integer total_number)
{
llLoadURL(llDetectedKey(0), "Visit the Bay City Post online",
"https://baycitypostsl.blogspot.com/");
}
}

This example is intended to go inside a fully transparent 0.5 m sphere so it can be activated by touch. Of course for another logo you'll want to change the texture UUID and hovertext as well as the prompting text and URL that's loaded when touched. If no touch-response is desired, the particle system will continue to show the logo after the script is removed, saving a tiny bit of simulator resources.

I've also used this approach to create an analogue "hoverclock" that shows the time from all sides, perched on top of teleport domes on major Virtual Railway Consortium sites around the SLRR. (The reader is invited to ponder the mystery of how the hands move around the face of those hoverclocks.)
Reporter Qie Niangao
191104

Bump 'n' Go: A simple Experience teleporter

I like to think it's useful for readers to learn about Experiences because Bay City has a high concentration of landowners, and landowners can enable Experiences on their parcel. There are also a large share of Premium subscribers, and they're eligible to define and use their very own custom Experiences.

Before enabling a custom Experience on your parcel, it must first be created -- an easy process, well documented in the knowledge base: https://community.secondlife.com/knowledgebase/english/experiences-in-second-life-r1365/#Section__4

One thing you can do with your own custom Experience is to make a simple collision-driven teleporter. Of course you could instead use other events besides collisions to trigger teleportation, but it's a neat trick to just walk through a fake door or fall through a "hole" in the floor and end up in a totally different place. So let's take the simplest collision-triggered Experience teleporter script as an example:

_______________

vector destiny = <56, 15, 2222>;
vector facing = <50, 15, 2222>;

default
{
state_entry()
  {
llVolumeDetect(TRUE);
  }
collision_start(integer num_detected)
  {
key collider = llDetectedKey(0);
if (ZERO_VECTOR == llGetAgentSize(collider))
return;
llRequestExperiencePermissions(collider, "");
  }
experience_permissions(key agent)
  {
llTeleportAgent(agent, "", destiny, facing);
  }
experience_permissions_denied(key agent, integer reason)
  {
if (17 != reason)
llRegionSayTo(agent, 0, "NO PERMS, reason: "+llGetExperienceErrorMessage(reason));
  }
}
_______________

That script would be placed in an object that, when the avatar steps into it, will teleport that avatar to a particular point in the same region (the "destiny" coordinates) oriented towards the "facing" coordinates when they arrive. So first you'll scope out where you want the avatar to go and which direction she should look on arrival, and change those two vectors to fit your situation.

(HINT: Don't set a "destiny" that's the EXACT same location of another teleporter because then you might arrive, collide with that teleporter, and be immediately teleported somewhere else -- or right back where you started. If BOTH ends specify each others' exact locations, it can be tricky to escape without relogging somewhere else.)

There are lots of ways to elaborate on this theme. For example, if this will serve the uninitiated, it wouldn't hurt to check first whether they're already enrolled in the Experience (llAgentInExperience), and if not, give them a little pep talk in chat before requesting experience permissions of them.

This form of teleporting also works for destinations in remote regions, but if you're unlucky, the destination region might be offline at the time. (There's also a separate step for finding the global coordinates of the remote region, and some trickiness with how the "facing" parameter is used, as well as a built-in "flyslow" animation that may need to be stopped.)

If there are a bunch of origin-destination pairs on a region, they might be networked to discover each other. (ADVANCED: They can even discover each other on very distant regions, using the "Key-Value Pair" persistent storage built-in to Experiences; that's how a network of walk-thru teleportation orbs work on Virtual Railway Consortium sites around the SLRR.)
Reporter Qie Niangao
190805

Calendar



Park Plaza Hotel

Park Plaza Hotel
Please visit our Sponsor!

Luxury Living in Bay City


Park Plaza residents enjoy beautiful views, fine dining, and a roof top patio with hot tub; all this near route 66, and within walking distance to Hairy Hippo Fun Land and aquarium. Rates start at 55L a week for studios, and 185L a week for full size apartments. Free furnishings available. Contact Roc Plutonium for more information!

Archive