Bouncing Ball Script (Transform Channels)

modo takes a somewhat unique approach to item transforms. Rather than include the transforms as part of the item itself, the item is linked to secondary transform-specific items. This article explains how transform channels work and demonstrates them with script that creates a bouncing ball.

Transform Channels

Every item in modo has channels, and these are the primary manner in which item state is exposed for editing. Some channels are hidden from the user, or represent [Common Datatypes#Complex Datatypes|complex datatypes]] such as mesh stacks and item links. In general, scripts deal only with Common Datatypes, notably numeric and string channels.

As described previously, most item channels can be walked through the sceneservice]] [[ScriptQuery Overview command. Transform channels, however, are a little different.

Transform Items

Locator item types (which includes ‘’Mesh’’, ‘’Camera’’, Light and so on) do not directly have their own position, rotation and scale channels. Instead, they have links to transform items that handle these properties. This allows multiple items to share the same transform items, and to save memory by not creating the transform items and associated channels when they aren?t required.

This unique system complicates how the position, rotation and scale of an item are read. You can?t directly ask the item for its channels, since they aren’t on that item. Instead, you must ask the item for its transform item, and then query that item’s channels. The sceneservice ScriptQuery interface provides a series of item.xfrm??? attributes to simplify getting the IDs of the transform items.

These each require an item ID as a selector, which can be obtained via the item.id attribute of ‘’sceneservice’’. The item.xfrmItems attribute returns a list of all transform items currently used by the selector item. For example, a mesh item might return the following:

1
2
3
 scale021
 rotation022
 translation023

You can also directly request a transform item by using one of the other item.xfrm??? attributes. These simply return the item ID string, or an empty string if there is no linked transform item.

As you can see from the table above, there are currently five types of transform items. The position, rotation and scale transforms should be obvious. Pivot defines a “pivot point”, which defines the position at which the rotation transform is centered. Pivot compensation is the inverse of the pivot transform; when you move the pivot of an item that has been scaled or rotated, a compensation transform is calculated to ensure that the item remains in the same wold position.

Creating Transform Items

If the transform you want to manipulate does not yet have a matching item, you need to create it first. This example shows how to check for the pivot transform item, and if it is not found, create it through the transform.add command.

Perl

my $itemID = myItemID;
my $pivotID = lxq( "query sceneservice item.xfrmPiv ? $itemID" );
lxout( "pivot = $pivotID" );
if( $pivotID eq "" ) {
    # Pivot item does not exist; create it
    lx( "transform.add type:piv" );
    $pivotID = lxq ("query sceneservice item.xfrmPiv ? $itemID" );
}

Lua

itemID = myItemID
pivotID = lxq( "query sceneservice item.xfrmPiv ? " .. itemID )[1]
lxout( "pivot = " .. pivotID );
if pivotID == "" then
    -- Pivot item does not exist; create it
    lx( "transform.add type:piv" )
    pivotID = lxq( "query sceneservice item.xfrmPiv ? " .. iitemID )[1]
end

Python

itemID = myItemID
pivotID = lx.eval1( "query sceneservice item.xfrmPiv ? " + itemID )
lx.out( "pivot = ", itemID )
if pivotID == "" :
    # Pivot item does not exist; create it
    lx.command( "transform.add" type="piv" )
    pivotID = lx.eval1 ("query sceneservice item.xfrmPiv ? " + itemID )

Once you have the item ID, you can query it as normal through the item.channel command or through the sceneservice interface. Since item.channel works on the current item selection, you need to select the transform item first with select.item, or you can pass it as the item argument. This example sets the X position of the pivot item to 1.3 meters.

Perl

lx( "item.channel pos.X 1.3 {$pivotID}" );

Lua

lx( "item.channel pos.X 1.3 " ..pivotID )

Python

lx.command( "item.channel", channel="pos.X", value="1.3", item=pivotID )

Bouncing Ball Example

This example perl script by Arnie Cachelin creates a simple bouncing ball style animation by creating keyframes on the position transform item of the currently selected item.

Perl

# perl -- Bouncer.pl
# Arnie Cachelin, Luxology LLC, Copyright 2007-2008
# A Simple Dynamics Script -- Physics 101 equations of
# motion in one dimension

# This version specializes in the trajectory of a dropped or
# launched object which bounces.
# To use this, select the object which is already at the time
# and place at its motion should start.


# User Input animation parameters
my $v0=0;           # initial speed (m/s)
my $time = 11.0;    # length of simulation (in seconds)
my $ymin = 0.0;     # ground/wall position, where bounces would occur (m)
my $elastic = 0.8;  # elasticity: percent energy kept per bounce
my $fstep = 0.2;    # number of frames between keys

# physical constants
my $g=9.8;          # gravity on earth = 9.8 m/s^2 DOWN
my $a=-$g;          # accleration (m/s^2) negative, since UP is positive

#lxtrace(1);         # Use this for debugging

# get selected locator-type item
my $item = lxq( "query sceneservice selection ? locator" );

# get its position transform item
my $xfrm = lxq ("query sceneservice item.xfrmPos ? $item");

if ($xfrm eq "") {
    # Position transform item does not exist; create it
    lx ("transform.add type:pos");
    $xfrm = lxq ("query sceneservice item.xfrmPiv ? $item");
}

# select the item in sceneservice for subsequent channel queries
my $xnm = lxq ("query sceneservice item.name ? $xfrm");

my $chan = lxq ("query sceneservice channel.index ?  pos.Y");.
my $tf = lxq ("time.range scene out:?");
$ti = lxq ("select.time ?");     # starting time: Now!
my $time = $tf - $ti;            # final time
my $val = lxq ("query sceneservice channel.value ? $chan");
my $fps = lxq ("time.fpsCustom ?");
my $dt = $fstep / $fps;          # add keys every other frame
my $nf = $time * $fps / $fstep;  # number of frames

my $y0 = $val - $ymin;           # initial position

#lxout ("Time = $time ( $nf frames at $fps fps ) from $ti s to $tf s by $dt s");    # Use this for debugging


# calculation section, where plenty of other important things are derived from the inputs

# maximum height reached, (where vtop==0)
my $ytop = ((0.5 * $v0 * $v0)/$g) + $y0;

# time to reach ytop from y0
my $ttop = ($v0)/$g;

# time to hit ground from ytop
my $tbounce = sqrt (($ttop * $ttop) + 2 * $y0 / $g);

# speed at bounce, deduced from energy conservation
my $vbounce = sqrt (($v0*$v0) + 2*$g*$y0);

#lxout ("Top at: $ttop s ($ytop m) Bounces $tbounce s later");


# add initial key at current time
lx ("channel.key mode:add channel:{$xfrm:$chan}");
lx ("select.channel mode:set channel:{$xfrm:$chan}");
lx ("channel.interpolation linear");
lx ("channel.value mode:set value:{$val}");

my $tscene = $ti;
my $tt;
my $y;

# time of next bounce relative to t0
my $tb = $ttop + $tbounce;
#lxout ("Bounce at = $tb s Top: $ttop s ($ytop m) ");

$t = 0;
for(my $i = 0; $i < $nf; $i++) {
    $t += $dt;

    # initial position
    $val = $y0;

    # plus initial constant velocity contribution
    $val += $v0 * $t;

    # plus acceleration contribution $val+=0.5*$a*$t*$t;
    if ($t>$tb) {
         # add key directly at bounce point!
         $tt = $tscene + $tb;
         lx ("select.time $tt");
         lx ("channel.key mode:add");
         lx ("channel.value mode:set value:{$ymin}");
         #lx ("key.break slope");
         #lx ("key.slopeType linearIn in");
         #lx ("key.slopeType linearOut out");
         # reset motion params
         $y0 = $ymin;         # start at bounce point
         $v0 = $vbounce;      # new initial velocity
         $tscene += $tb;      # reset time so t=0 at bounce
         $t=$t-$tb;           # next key 't' a bit after bounce

         # attenuate t,v
         $tbounce = sqrt ($elastic) * $tbounce;
         $vbounce = sqrt ($elastic) * $vbounce;

         # time up and time down
         $tb =  2 * $tbounce;

         # recompute key after bounce
         $val = $y0;

         # plus initial constant velocity contribution
         $val += $v0 * $t;

         # plus acceleration contribution
         $val+=0.5*$a*$t*$t;
   }

   # use absolute time, not per-bounce time
   $tt = $tscene + $t;

   # convert relative y back to absolute
   $y = $val + $ymin;

   # set time, add and set key
   lx ("select.time $tt");
   lx ("channel.key mode:add");
   lx ("channel.value mode:set value:{$val}");
}

Lua

-- lua -- Bouncer.lua
-- Arnie Cachelin, Luxology LLC, Copyright 2007-2008
-- A Simple Dynamics Script -- Physics 101 equations of
-- motion in one dimension

-- This version specializes in the trajectory of a dropped or
-- launched object which bounces.
-- To use this, select the object which is already at the time
-- and place at its motion should start.


-- User Input animation parameters
v0=0           -- initial speed (m/s)
time = 11.0    -- length of simulation (in seconds)
ymin = 0.0     -- ground/wall position, where bounces would occur (m)
elastic = 0.8  -- elasticity: percent energy kept per bounce
fstep = 0.2    -- number of frames between keys

-- physical constants
g=9.8          -- gravity on earth = 9.8 m/s^2 DOWN
a=-g           -- accleration (m/s^2) negative, since UP is positive

--lxtrace(1)   -- Use this for debugging

-- get selected locator-type item
item = lxq( "query sceneservice selection ? locator" )[1]

-- get its position transform item
xfrm = lxq ("query sceneservice item.xfrmPos ? "..item)[1]

if xfrm == "" :
    -- Position transform item does not exist; create it
    lx ("transform.add type:pos")
    xfrm = lxq ("query sceneservice item.xfrmPiv ? " ..item)[1]
end

-- select the item in sceneservice for subsequent channel queries
xnm = lxq ("query sceneservice item.name ? "..xfrm)[1]

chan = lxq("query sceneservice channel.index ?  pos.Y")[1]
tf = lxq ("time.range scene out:?")[1]
ti = lxq ("select.time ?")[1]  -- starting time: Now!
time = tf - ti                 -- final time
val = lxq ("query sceneservice channel.value ? "..chan)[1]
fps = lxq ("time.fpsCustom ?")[1]
dt = fstep / fps               -- add keys every other frame
nf = time * fps / fstep        -- number of frames

y0 = val - ymin                -- initial position

--lxout ("Time = "..time.." ( "..nf.." frames at "..fps.." fps ) from "..ti.." s to "..tf.." s by "..dt.." s")    -- Use this for debugging


-- calculation section, where plenty of other important things are derived from the inputs

-- maximum height reached, (where vtop==0)
ytop = ((0.5 * v0 * v0)/g) + y0

-- time to reach ytop from y0
ttop = (v0)/g

-- time to hit ground from ytop
tbounce = sqrt ((ttop * ttop) + 2 * y0 / g)

-- speed at bounce, deduced from energy conservation
vbounce = sqrt ((v0*v0) + 2*g*y0)

--lxout ("Top at: "..ttop.." s ("..ytop.." m) Bounces "..tbounce.." s later")


-- add initial key at current time
lx ("channel.key mode:add channel:{"..xfrm..":"..chan.."}")
lx ("select.channel mode:set channel:{"..xfrm..":"..chan.."}")
lx ("channel.interpolation linear")
lx ("channel.value mode:set value:{"..val.."}")

tscene = ti
tt = 0
y = 0

-- time of next bounce relative to t0
tb = ttop + tbounce
--lxout ("Bounce at = "..tb.." s Top: "..ttop.." s ("..ytop.." m) ")

t = 0
for i=0,nf do
    t += dt

    -- initial position
    val = y0

    -- plus initial constant velocity contribution
    val += v0 * t

    -- plus acceleration contribution val+=0.5*a*t*t
    if t > tb  then
         -- add key directly at bounce point!
         tt = tscene + tb
         lx ("select.time " ..tt)
         lx ("channel.key mode:add")
         lx ("channel.value mode:set value:{"..ymin.."}")
         --lx ("key.break slope")
         --lx ("key.slopeType linearIn in")
         --lx ("key.slopeType linearOut out")
         -- reset motion params
         y0 = ymin       -- start at bounce point
         v0 = vbounce    -- new initial velocity
         tscene += tb    -- reset time so t=0 at bounce
         t=t-tb          -- next key 't' a bit after bounce

         -- attenuate t,v
         tbounce = sqrt (elastic) * tbounce
         vbounce = sqrt (elastic) * vbounce

         -- time up and time down
         tb =  2 * tbounce

         -- recompute key after bounce
         val = y0

         -- plus initial constant velocity contribution
         val += v0 * t

         -- plus acceleration contribution
         val+=0.5*a*t*t
   end

   -- use absolute time, not per-bounce time
   tt = tscene + t

   -- convert relative y back to absolute
   y = val + ymin

   -- set time, add and set key
   lx ("select.time "..tt)
   lx ("channel.key mode:add")
   lx ("channel.value mode:set value:{"..val.."}")
end

Python

# python -- Bouncer.py
# Arnie Cachelin, Luxology LLC, Copyright 2007-2008
# A Simple Dynamics Script -- Physics 101 equations of
# motion in one dimension

# This version specializes in the trajectory of a dropped or
# launched object which bounces.
# To use this, select the object which is already at the time
# and place at its motion should start.

from math import sqrt

# User Input animation parameters
v0 = 0            # initial speed (m/s)
ymin = 0.0     # ground/wall position, where bounces would occur (m)
elastic = 0.8   # elasticity: percent energy kept per bounce
fstep = 2.0    # number of frames between keys

# physical constants
g = 9.8          # gravity on earth = 9.8 m/s^2 DOWN
a = -g           # accleration (m/s^2) negative, since UP is positive

#lx.trace(1)   # Use this for debugging

# get selected locator-type item
item = lx.eval1( "query sceneservice selection ? locator" )
if not item:
    sys.exit("LXe_FAILED: Need to select an item")

# get its position transform item
xfrm = lx.eval1( "query sceneservice item.xfrmPos ? " + item )

if not xfrm:
    # Position transform item does not exist; create it
     lx.command ("transform.add", type="pos")
     xfrm = lx.eval1 ("query sceneservice item.xfrmPos ? " + item)

# select the item in sceneservice for subsequent channel queries
xnm = lx.eval1 ("query sceneservice item.name ? " + xfrm)

chan = lx.eval1 ("query sceneservice channel.index ?  pos.Y")
tf = lx.eval1 ("time.range scene out:?")
ti = lx.eval1 ("select.time ?")     # starting time: Now!
time = tf - ti              # final time

val = float(lx.eval1 ("query sceneservice channel.value ? " + chan)) #bug: returns string value
fps = lx.eval1 ("time.fpsCustom ?")
dt = fstep / fps            # add keys every other frame
nf = time * fps / fstep     # number of frames

y0 = val - ymin              # initial position

#lx.out ("Time = ", time, " ( ", nf, " frames at ", fps, " fps ) from ", ti, " s to ", tf, " s by ", dt, " s",    # Use this for debugging


# calculation section, where plenty of other important things are derived from the inputs

# maximum height reached, (where vtop==0)
ytop = ((0.5 * v0 * v0) / g) + y0

# time to reach ytop from y0
ttop = v0 / g

# time to hit ground from ytop
tbounce = sqrt ((ttop * ttop) + 2 * y0 / g)

# speed at bounce, deduced from energy conservation
vbounce = sqrt ((v0 * v0) + 2 * g * y0)

#lx.out ("Top at: ", ttop, " s (", ytop, " m) Bounces ", tbounce, " s later")


# add initial key at current time
lx.command ("select.channel", mode="set", channel=xfrm + ":" + chan)
lx.command ("channel.interpolation", type="linear")
lx.command ("channel.key", value=val, mode="add")

tscene = ti
tt = 0
y = 0

# time of next bounce relative to t0
tb = ttop + tbounce

#lx.out ("Bounce at = ", tb, " s Top: ", ttop, " s (", ytop, " m) ")

t = 0
for i in range(nf):
    t += dt

    if t > tb:
        # add key directly at bounce point!
        tt = tscene + tb
        lx.command ("channel.key", time=tt, value=ymin, mode="add")

        # attenuate t,v
        tbounce = sqrt (elastic) * tbounce
        vbounce = sqrt (elastic) * vbounce

        # reset motion params
        y0 = ymin         # start at bounce point
        v0 = vbounce      # new initial velocity
        tscene += tb      # reset time so t=0 at bounce
        t=t-tb          # next key 't' a bit after bounce

        # time up and time down
        tb =  2.0 * tbounce

    # recompute key after bounce
    val = y0

    # plus initial constant velocity contribution
    val += v0 * t

    # plus acceleration contribution
    val += 0.5 * a * t * t

    # use absolute time, not per-bounce time
    tt = tscene + t

    # convert relative y back to absolute
    y = val + ymin

    # set time, add and set key
    lx.command ("channel.key", time=tt, value=y, mode="add")