USER FUNCTIONS

Janus's user functions are one of the more complex features that can be hard to understand. This section dedicates the topic of how to use and write user functions in the Janus context.

This section is divided into

User Function Overview

User functions are like variables but you use LScript to determine what the value is. This makes it extermely powerful, and convenient to configure Janus for a multitude of tasks, and in conjunction with other constructs (like FOR loops), automate Janus to a high degree.

In Janus, a user function call is made by enclosing a function call in func::. Example:

NOM.BG.func:test_function(myarg):.targetgroup

where test_function() is the function, and myarg is the argument being passed into the function. myarg doesn't need to exists, though be mindful that when writing user functions (explained below), there are mandatory and implicit arguments being passed to the user function anyway.

This test_function() is a function that exists (or should exist) in the Janus_UserFunctions.ls. In this .ls file, there is already a pre-written header portion (that should not be modified), that deals with its communication with Janus. It is necessary to keep the this part of the Janus_UserFunctions.ls un-edited to make it work properly. In a section that has been marked, the Janus user can add new functions and define them.

The nice thing about user functions in the way they've been implemented is that you don't have to restart Janus to debug your user functions, since user functions are called and exited as they are processed, and they exist outside the scope of Janus code (to the degree that they are not dependent on Janus's own internal state).

User Function Contexts

User functions have contexts. This just means that a user function can appear in different parts of Janus, such as when a user function is used in the cmd line, or inside in a string field parameter in the Preferences, or even inside a PSO (partial-surface override -- Janus_SurfaceOverrides.txt).

In essence, Janus currently has two contexts: the cmd line context, and the PSO (Partial Surface Overrides) context.

For each context, user functions may behave differently. User functions also pass different arguments to your functions (aka Input Arguments) depending on the context. Also, it's important to know what value your user function is expected to return for a particular context, or what return values signify in that context.

Overview of Input Arguments

Input arguments are implicit and mandatory arguments that are supplied to your user functions. Because they are mandatory, you must take care in using your argument list when you are writing user functions. These mandatory arguments are always placed at the last part of the arglist[] array as supplied by Janus. These mandatory arguments are discussed in detail in the specific contexts below.

Overview of Return Values

There are three generic return values to be aware of.

  1. literal string - since most of the Janus's data gets contextualised by LW internally (eg via its own .lws reading mechanism), all valid data should be represented by strings, so that even the string "1", if meant to be an the integer, will be interpretted as such, as long as it conforms to what Janus expects that value to be after the user function has processed it. Ex: return(string(result));
  2. empty or null string ("") - a null string will tell Janus to clear the user function, explicitly setting the resulting value to nothing. For example, if you input: NOM.BG.func:test_function():.targetgroup, and where JNUF_test_function() return a null string, the resulting cmd line will look like: NOM.BG..targetgroup.
  3. -1 integer - if you specify a negative 1 integer (eg return(-1)), this tells Janus to pretend it never processed the user function, and keeps the original line intact. If we were to use the same JNUF_test_function() example as #2, if JNUF_test_function() returned a -1, the resulting cmd line would be NOM.BG.func:test_function():.targetgroup -- basically the same cmd line.

There are other possible return values in other contexts below.

Cmd Line Context Overview

The command line (cmd line) context is user functions being called from the cmd line. This also means any user function that ends being in the cmd line through command type inheritance.

Cmd Line Input Arguments

There is only one cmd line input argument, and that's the render pass index which also refers directly to the internal array of render passes. This value is passed as the last element in the arglist[]. For example, if you call a function such as:

func:test_function(101):

You must be cognizant of the fact that Janus will pass two arguments to your user function: the string "101", and the render pass index that the user function belongs to. So that if you create your user function, it should look like this:

JNUF_test_function: arglist
{
   mystring = arglist[1]; // 101
   comndx = arglist[2]; // current index of render pass being processed
}

Cmd Line Return Values

Besides the generic return values mentioned above, the cmd line context can return the string "__ABORT__" (two underscores, the string "ABORT", followed by another two underscores). This tells Janus to not process this particular render pass for breakout, or test-rendering.

(This example is demonstrated in a Janus user function called JNUF_searchForSurfaceNameInActiveGroup. This user function's purpose is to look for a particular surface name amongst the items of the active group, and if it doesn't find it, it aborts the breakout. This was to be used in conjunction with composite command types and its overall strategy was to allow the user to export render passes with with particular surface names instead of manually associating and de-associating specific objects to the render pass.)

Partial-Surface Override (PSO) Context Overview

The PSO context is user functions being called from inside the Janus_SurfaceOverride.txt in the form of partial-surface overrides. This context is obviously Surface-oriented and gives provides additional arguments to user functions related to the surface and object being processed at that time.

It's helpful to know how Janus applies partial-surface overrides to objects with or without the use of user functions, because it's in this processing that user functions are applied. When PSOs are processed, a list of surface channel exclusions (derived from the exclude directives) are collected; we'll call this the exclude collection phase. The same proces goes for setvalue directives, and null directives.

When the exclude, setvalue, and null collection phase are done, the exclusion list is queried for user functions (if user functions exist), and it is modified as directed.

Then for each object, Janus goes through every surface channel. If the surface channel is part of this exclusion list, then Janus ignores it and moves on. If the surface channel is to be processed, then it gets further information from the setvalue list collection, and if it finds the directed channel it processes the value for that channel. If the value is a user function, ie:

setvalue=Transparency=func:setValueOfSurfaceName(Frame,endswith,1):

then the user function is processed and the value replaced.

PSO Input Arguments

In PSO context there are two mandatory arguments:

  1. Surface name
  2. Object name

Using the following function:

func:test_function_pso(101):

Your function could look like this:

JNUF_test_function_pso: arglist
{
   mystring = arglist[1]; // 101
   surface_name = arglist[2]; // current surface name being processed
   item_name = arglist[3]; // current item (mesh) being processed
   mesh = Mesh(item_name); // mesh OA derived from string
   surface = Surface(surface_name,mesh.id); // surface OA from surface name and mesh OA
}

PSO Return Values

There are no special return values in the PSO context. Using the generic return values yields the same behaviour as already mentioned above.

However, note that in the PSO context, you have a PSO directive called null, which doesn't do anything to any return value; it simply executes the user function.

Creating User Functions

Before discussing how to create user functions, it will be useful to know how Janus runs and feed data to the user functions.

User function execution overview

When a function call has been made it Janus first by opens up an LScript Queue (identified as 'JNQ') which Janus populates with the function name, a slot or space for the return value of the function, and the arguments that will be passed into the function. After Janus has populated the Queue with data, it calls Janus_UserFunctions.ls.

In the Janus_UserFunctions.ls code, the 'JNQ' Queue is referenced and the script retrieves the function name and the arguments. Then it checks to see if the function name exists in an internal list of known functions. If it doesn't see it, then Janus will throw a warning message.

If the function exists, then Janus will call it, expecting a single return value. Return values can be any data type except arrays. However, because the return value is only useful in Janus as a string, it effectively converted to strings.

Calling a user function looks like calling a subcommand variable. But instead of using the 'sc:' keyword, 'func:' is used instead.

func:functionName():

A function can receive as many arguments as you wish. And they are called as if they were written in LScript. Example:

func:getBaseSceneName(%JN_SCENENAME%):

The function getBaseSceneName() as one argument, though more than one is allowed. But where is getBaseSceneName()? All user functions called by Janus must reside in the file Janus_UserFunctions.ls (this file is provided for in the Support Files zip), and should be located in the support folder.

Important note: Janus_UserFunction.ls must be installed as a plugin in LightWave so that Janus can call it directly.

Janus_UserFunctions.ls contains initialisation code that should not be modified since they communicate directly with Janus's events. Below is an explanation of how the user functions system works in Janus and how to create new functions.

How to create new user functions

When creating new user functions, it's most helpful to have a small checklist of what needs to be done, and what to watch out for.

  1. User functions must be begin with JNUF_.
  2. 'Register' (or declare) the new user function by locating the function initFunctionList inside Janus_UserFunctions.ls.
  3. Create user functions only below the section specified in Janus_UserFunctions.ls (eg where the comment "// MODIFY ONLY CODE BELOW THIS LINE // "is marked)
  4. User functions can be called with multiple arguments, such as:

    func:test_function(%JN_PASSNAME%,1,10):
  5. Related to #3, remember that arguments are passed into functions as a single array (in Janus distributions this is a default variable name called arglist). You have to break apart the 'arglist' array to get to the individual passed arguments. So that if you had passed three (3) arguments in the aforementioned test_function(), this is how your user function will look like from a cmd line context:

    JNUF_test_function: arglist
    {
       passname = arglist[1]; // %JN_PASSNAME% resolved into pass name before script function is called
       var2 = arglist[2]; // string "1"
       var3 = arglist[3]; // string "10"
       comndx = arglist[4]; // render pass index
    }
  6. All return statements must be resolvable to strings (ie no arrays). The return value is passed onto the Janus system.

JNUF_exampleFunction: arglist
{
    arg1 = arglist[1]; // %JN_PASSNAME%
    arg2 = arglist[2]; // 1
    arg3 = arglist[3]; // 10
    result = arg1+arg2+arg3+arg4;
    return(result);
}

ComRing in User Functions

ComRing capability allows user functions to query Janus some of its internal data making user functions more useful.

ComRing Overview

The ComRing interface has been implemented in user functions in the comringcall function found in Janus_UserFunctions.ls. This function has been designed to work especially with Janus's own internal 'receiver' of ComRig data. Users who would write functions using ComRing should use this function as a bridge to Janus.

comringcall accepts three arguments: a particular Janus querying command (explained below), and two integers. These integers represent two array dimensions; at the moment only the first integer argument is used, and will be demonstrated below.

ComRing Usage

The ComRing mechanism is generally used to query Janus about some of its data. For example, if you you want to know how many associated groups a current render pass has, you can query using the string "renderpass.assocgroups.size". This query command is passed to comringcall along with the render pass's index (optional). It looks something like this:

comndx = integer(arglist[2]); // render pass index; cmd line context
result = comringcall("renderpass.assocgroups.size",comndx,1);

So as you can see, the comringcall function takes care of the other bits of code that Janus specifically needs to process the query; all you need to do is determine the query command, and if needed, input the render pass index in question.

ComRing Query Commands

The following are the possible data that can be queried from Janus. Note that the formatting of these commands are reminiscent of object-oriented syntax, though it has not been internally designed that way. However, in order to make more sense, it will explained as though they were objects. it follows a system of hierarchy, and in this documentation, the relationship will be represented by its identation in the list.

Some notable things to point out: because you are able to specify a number for referencing a particular render pass (eg renderpass.#), it is not always necessary to feed the comringcall with the second argument, which Janus internally regards as the render pass index. However, because Janus will consistently/reliably pass its render pass index via the last argument (in the cmd line context, at any rate), you can use this second argument as insurance against possible errors with render pass index referencing.