SVG Canvas: A way to generate dynamics graphics on prim faces (without MoaP)
tracked
Jenna Felton
Motivation:
Second life is full of information and very often you have to display some information dynamically: Radars, navigation signs, fancy menus, information boards, HUD applications. And that is a rough listing.
For doing this we have prepared textures, MoaP, hover text. Textures can use a wast number of faces if you want to display an arbitrary text. MoaP is doable but has problems with transparency. And security concerns.
Hover text works well but it can not be limited to a certain area. Like not at all.
SVG Canvas tries to counter these issues and to give a way to create simple graphical information dynamically (but it will be much richer then a simple hover text).
Basic idea
A SVG canvas is a container for a SVG image and provider of its content: The container is created by a script, it has an own UUID which can be applied to any prim face like a regular texture. To display this image the viewer receives the SVG content from this container.
The SVG canvas is stored within the heap memory of the script created it (parent script). As long the script is available, the region can provide the SVG content to the interested viewer. Thus, the SVG canvas is best used on HUDs and in-world objects also running the parent script.
The canvas is created via the call
key llSVGCanvas(list options);
The options list defines the
<?xml>
header (the only way to set the <?xml>
attributes).The result key is used in the following commands and is also applied to prim faces.
To check the current status of the canvas we use the function
integer llSVGStatus(key canvas);
The function checks if the key is a SVG canvas, if it was created by the same script, if it is stil valid etc. Result is 0 or error code.
Since SVG is a XML dialect, to build/draw the SVG image we use DOM commands, but a non-OOP way. The commands can only be called within the parent script (for now).
integer llSVGGetElementByID(key canvas, string id);
This command finds the element having the given "id" attribute and returns its number. Each SVG element is addressed by a distinct number maintained internally, which can but must not be saved in the "id" attribute explicitly. For example we can use the order in which the elements were added while the top-level
<svg>
node has the element number 0.integer llSVGSetAttributes(key canvas, integer element, list attribs);
integer llSVGUpdateAttributes(key canvas, integer element, list attribs);
list llSVGGetAttributes(key canvas, integer element);
The commands set or read attributes of the SVG element given by the number. The attributes are given by name and value (alternating). The .Set. command replaces the element attributes, the .Update. command only changes values of named attributes. Both return a negative value on error and the (same) element number on success.
integer llSVGSetXML(key canvas, integer element, string xml);
string llSVGGetXML(key canvas, integer element);
This command sets and retrieves the XML content of the given element. This is used to change the text of the
<text>
element but also allows to draw the <svg>
image at once when desired this way.integer llSVGAddElement(key canvas, integer parent, integer sibling, string tag, list attribs, string XML);
This call creates a new SVG element with given tag, attributes, and content and places it is as child element of the given parent element
after
the element given by sibling when it is an element of the parent element. Otherwise the new element is placed after all child elements of the parent. The result is the number of the new element or negative on error. This operation may change numbers of some elements. The top-level <svg> node exists by default and must not be created.integer llSVGRemoveElment(key canvas, integer element);
This operation removes the SVG element having the given number. The top-level
<svg>
node can not be removed.Extensions
- Besides the simple elements (path, text etc.) we can also have commands for drawing of UI elements. The commands will provide only logical definition while the viewer will render the elements with respect of the current skin. This will allow to create apps with a look and feel of viewer floaters while they will operate on scripts running on the region.
- Since some of these elements must be clickable, the elements can be added proprietary attributes working like interaction listens and eg. on touch triggering the event
on_svg_event(key object, key canvas, integer element, integer action)
Within this event one could call then llDetected... functions for further information.
- Instead of creating the feature for SVG only we can first create the framework for building and updating a XML documents and use this to define SVG images among other uses.
Example follows in a comment.
Log In
Spidey Linden
Merged in a post:
Per face texture compositing (stateless sprite layers)
Send Starlight
Draw an ordered list of textured quads onto a single face each frame stateless so the result stays consistent across all viewers. Let a script assign a face an ordered, declarative list of quads sprites. The list is the face's appearance, it is recomputed from the list, never accumulated so every viewer, late joiner, and relog renders identical. Today a prim face can only reference a single texture asset PRIM_TEXTURE sampled with UV repeats/offsets. This would unblock an entire class of 2D content: game screens, animated signage, live dashboards, scoreboards, paper doll/outfit previews, map overlays, and HUDs.
Instead of keeping the old pixels and overlay new ones which is a stateful, accumulating buffer, let the face hold a declarative list of layers. The face's image is fully determined by the current list and the cached source textures it references, the same way PRIM_TEXTURE, color, and other prim params already determine appearance. The viewer composites the list to produce the face; it does not accumulate across calls. Because the appearance is a pure function of replicated object state. Every viewer composites the same list and gets the same image. A late joiner receives the current list and renders correctly with no replay. A relog reconstructs from the list, no buffer to persist. There is no draw history to synchronize. This is the key difference from every writable texture proposal: nothing is ever written and kept. The picture is rebuilt from a declarative spec.
A new primitive parameter, e.g. PRIM_FACE_COMPOSITE, usable with llSetLinkPrimitiveParamsFast so it can be batched like everything else:
[ PRIM_FACE_COMPOSITE, integer face, list layers ]
where layers is a strided list, each layer being:
string texture // UUID or inventory name; cached like any normal texture
vector srcUV // <u, v, 0> top left of the source rect within the texture (0 .. 1)
vector srcSize // <w, h, 0> size of the source rect (0 .. 1) lets you pull one sprite out of a spritesheet/atlas
vector dstUV // <u, v, 0> where to place it on the face (0 .. 1)
vector dstSize // <w, h, 0> how big to draw it on the face (0 .. 1)
float rot // rotation in radians about the quad center
vector color // tint
float alpha // opacity
integer blend // 0 = alpha, 1 = additive (optional default alpha)
Layer order in the list = back to front draw order. Passing an empty list clears the face back to its normal single texture.
Example: a parallax background plus two sprites on face 0:
llSetLinkPrimitiveParamsFast(LINK_THIS, [
PRIM_FACE_COMPOSITE, 0, [
bg, <0,0,0>, <1,1,0>, <0,0,0>, <1,1,0>, 0.0, <1,1,1>, 1.0, 0,
sheet, <0,0,0>, <0.125,0.125,0>, <0.4,0.5,0>, <0.1,0.1,0>, 0.0, <1,1,1>, 1.0, 0,
sheet, <0.25,0,0>, <0.125,0.125,0>, <0.6,0.5,0>, <0.1,0.1,0>, 0.0, <1,0.7,0.7>, 1.0, 0
]
]);
A query form llGetLinkPrimitiveParams with PRIM_FACE_COMPOSITE would return the current list.
2D games drawn on a single prim instead of a pool of quads, dramatically lower Land Impact and link count, and it sidesteps the 256 prim cap. Animated/dynamic signage and vendors, composite product images, prices, and badges on one face without prim stacks. Live scoreboards, dashboards, leaderboards, text rendered as glyph sprites from an atlas the way text on a prim systems already fake it, but on one face and consistent. Outfit / paper doll previews, map overlays, mini maps, status HUDs.
Bounded by design. Cap layers per face (e.g. 64 – 256) and total composited faces per linkset, reject or clamp beyond the cap. The composite only needs to rerun when the list or a source texture changes, not every frame, animation is just the script updating the list. No new asset pipeline. Sources are ordinary cached texture assets referenced by UUID, no uploads, no server side rendering, no external fetch, no IP exposure. VRAM is capped, not unbounded. Composite to a fixed modest target resolution per face (e.g. 512 x 512, configurable/limited), and only for opt in faces, so total render target memory has a hard ceiling rather than scaling with prim count. Deterministic. Identical list, identical cached textures and identical output on every viewer.
SL Feedback
marked this post as
tracked
SL Feedback
Issue tracked. We have no estimate when it may be implemented. Please see future updates here.
Journey Bunny
Would this (currently under review) serve the purpose and more? https://feedback.secondlife.com/feature-requests/p/svg-canvas-a-way-to-generate-dynamics-graphics-on-prim-faces-without-moap
Bambi Bubbles
Jenna Felton
Hello Bambi Bubbles :)
Yes, it is very related. In the linked post was a method suggested to render an arbitrary text over a surface. Which will be sufficient for many applications, but there was also an idea to use SVG which can allow dynamic (or temporary) graphics drawn on surfaces and it will allow even more applications.
So, the idea was to introduce a SVG canvas that holds the content of the image inside a script memory (i think it is the most optimal location) and provide a number of functions that manipulate this content in a simple way.
But both suggestions aim the the goal in the same direction, i think.
Bambi Bubbles
Jenna Felton There's definitely plenty of overlap between both feature requests, and if a project were planned to do one, it would probably be advantageous to incorporate 'doing both' at the same time.
And it would make for a great new 'feature package' with both those things together.
Spidey Linden
marked this post as
under review
Jenna Felton
The text needs an example but it was too long for canny, so, here is a comment :)
To define a simple SVG image, we could use this call set:
key canvas = llSVGCanvas([
"version", "1.0", "encoding", "UTF-8",
"standalone", "no"]);
if (llSVGStatus(canvas) < 0) return;
// Let's assume the rest will run ok.
// Element #0 is the top level svg node
llSVGSetAttributes(canvas, 0, [
"xmlns:svg", "http://www.w3.org/2000/svg",
"xmlns", "http://www.w3.org/2000/svg",
"version", "1.0",
"width", "220",
"height", "220"]);
// There is no nesting, the parent is top-level 0 and
// using -1 as sibling ads the elements one after one
integer rect = llSVGAddElement(canvas, 0, -1, "rect", [
"width", "66", "height", "30",
"x", "21", "y", "32",
"stroke", "#204a87",
"stroke-width", "2", "fill", "none"], "");
integer text = llSVGAddElement(canvas, 0, -1, "text", [
"x", "21", "y", "82"], "Rectangle");
This code will create an SVG image with this content (instead of calling
llSVGAddElement
function two times we can call llSVGSetXML
command once with the content of the <svg> node if we want so.):<?xml version="1.0"
encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.0" width="220" height="220">
<rect width="66" height="30" x="21" y="32"
stroke="#204a87" stroke-width="2" fill="none"/>
<text x="21" y="82">Rectangle</text>
</svg>
But if we'd generate the image by direct providing the XML content of the
<svg>
node, we'd need to give the nodes rect
and text
some distinct ID's and then ask their element numbers via the llSVGGetElementByID
function afterwards.Because now we can do this: Whenever we want, we can change the text of the text node:
llSVGSetXML(canvas, text, "click this");
And we also add a touch listen to the rectangle:
llSVGUpdateAttributes(canvas, rect, ["event", "SVG_TOUCH"]);
which is a proprietary attribute and will trigger an event on touch we can handle:
on_svg_event(key obj, key canv,
integer elem, integer action) {
if (action == SVG_TOUCH)
if (elem == rect)
if (canv == canvas)
{
llOwnerSay("thanks 4 clicking!");
}
}
Jenna Felton
Sorry for talking to myself :) Actually the topics is a bit complicated and the intention was to give an initial idea and then when it is as good as I think it is there will be discussions and final details will be talked out. The code was meant as initial suggestions. But some thinking lead to some suggestions :)
I think it makes sense to separate the SVG document and the top level
<svg>
node. The latter will be created automatically (to save one call) and receives the element number 1, while the the whole document the element number 0.This will allow an access to the whole document (by using the element number 0) and to the top-level
<svg>
note separately, by using element number 1.Access to the XML content of a node
including the tags
of the element (outer XML) via functionsstring llSVGGetXML(key canvas, integer element);
integer llSVGSetXML(key canvas, integer element,
string XML);
Access to the part between the element tags (inner XML) via functions
string llSVGGetContent(key canvas, integer element);
integer llSVGSetContent(key canvas, integer element,
string XML);
Access to attributes like above. To create the new nodes we'll need the functions
integer llSVGAddChild(key canvas, integer element,
integer append, string tag, list attribs,
string XML);
The function creates a new element node as a child node of the given element. The new node is placed after (append = TRUE) or before (append = FALSE) the other child nodes. Result is the number of the new element or error code.
integer llSVGAddSibling(key canvas, integer element,
integer append, string tag, list attribs,
string XML);
This function creates a new element and inserts it after (append = TRUE) or before (append = FALSE) the given element as child element of its parent, i.e. as a sibling element of the given one.
Removing an element via
llSVGRemoveElment
as explained. I think that must be enough to create SVG image even using CSS. So I stop adding suggestions now, and i am curious about other people suggestions :)
c
coldheartmonster Resident
Jenna Felton I find many interesting things that could be done (math, ML...), In SL however drawing or writing on a prim is something we some of us been waiting for 20 years. https://github.com/Martin-Pitt/NexiiText4 (accepts many formats JSON, XML, rrss ...) there are a few inventions just like xytext that permit functionalities. What I love of SL is that there's infinite ways to build. [in the image text displayed from a notecard low land impact]
Photo Viewer
View photos in a modal
Jenna Felton
Hi coldheartmonster Resident :)
Thank you for your reply :) Yes it is great that there are many ways to build things in SL and you can choose whatever it is more appropriate for a task you are doing.
Thank you also for linking NexiiText :) I have not figured out yet how to run it but with more time I will :) The project seems very interesting.
Send Starlight
Jenna Felton Your example is a clear example why we need a low-RAM render text solution separate from SVG support still. They don't give us much RAM for scripts.