Scripting Using the Resource Palette and Script FX

The Script FX shader has more generalized and powerful scripting potential than the node scripts discussed in the previous section, Scripting in the Project Window and Instance Nodes. From this scripting location you can modify nearly everything about the object your Script FX shader is attached to, and it's easy for the user to drag and drop these shaders onto any selectable object in the modeling view.

To create a new Script FX shader in an existing document go to the Resource palette and click on the FX tab. Then click on the New popup menu button and select Script FX from the menu. You'll find yourself in the standard Script FX editing dialog.

The key to writing Script FX shaders is to understand the s3d.render.EntityHandler class. Nearly everything you'll want to do from a Script FX shader will involve modifying the state of the entity handler, from changing the current time to applying custom transformations to inserting geometry into the rendering pipeline. You may want to read that documentation before continuing with this section.

The Script FX Dialog

A standard textual script editing dialog is presented for new Script FX shaders and for most existing shaders, although the dialog can be customized for existing shaders. Create a new Script FX shader or chose to edit an existing one to bring up this dialog:

The Initialization section contains Lua code executed whenever the script is re-evaluated. This occurs when the preview rendering camera button is hit, when this dialog is exited via the OK button, or when a file containing the script is loaded from disk. It might happen at other times too, but the basic idea is that the initialization is run once as far as this script is concerned, while the body is run many, many times. It provides a good place to put things that don't need to be re-evaluated every time the shader is asked to run.

The Script Source area has a label above it indicating which parameters are passed in, which at the time of this writing are context, prepare, pixel, final, and cleanup.

The context argument holds a transient integer value unique to each instantiation of the shader. Don't depend on these values remaining the same across different renderings. For example if you drag and drop a Script FX from the resource palette onto five different objects the context value will be different as the script is run for each object.

The other arguments will be non-nil one at a time in various execution phases, providing a table of named keys and values specific to each phase.

Custom Script FX Dialogs

Like the Instance Node Scripts, Script FX shader scripts may supply their own dialog for editing. The mechanisms are very similar with the primary difference being the arguments expected for the function implementing the dialog. An example follows of a custom dialog for a shader that essentially does nothing.

You can copy and paste the following text into the correct sections of a Script FX shader, or you can download this model file which has both a Script FX shader with a custom dialog and a scale node script with a custom dialog in it.

Copy and paste this content into the Initialization section of a Script FX script.

-- For the Initialization section.

--

-- Tag a section for parameters we're going to edit in our dialog.

-- There's nothing magic here, just comments in our source code to bound things.

--

--@EDITABLE_PARMS_BEGIN

local parms = { message = [[This text will be remembered next time around.]] }

--@EDITABLE_PARMS_END

--[[@CUSTOM_EDITOR

--

-- Bail out and present the normal text editor if the option/alt key is held down.

--

if s3d.ui.IsModifierKeyDown(s3d.ui.modifiers.alt) then return nil end

local scriptfx = require "scriptfx"

--

-- Provide a custom editor for this script.

--

return function (shader, name, initialization, functionBody)

--

-- Load the parameters from this very initialization text

--

local parms, startParms, endParms = scriptfx.dialog.GetEmbeddedScriptParams( initialization )

--

-- Build the dialog

--

local d = s3d.ui.Dialog( "Do Nothing Script FX" )

d:BeginCluster("", true, false, 0)

local l1 = d:AddStaticText( "Shader Name: ", 80 )

d:AddColumn(0)

local e1 = d:AddEditText( name, 300 )

d:EndCluster()

d:AddStaticText( "" ) -- Just for some vertical space.

d:BeginCluster("", true, false, 0)

local l2 = d:AddStaticText( "Message: ", 80 )

d:AddColumn(0)

local e2 = d:AddEditText( parms.message, 300 )

d:EndCluster()

d:AddStaticText( "" ) -- Just for some vertical space.

local l3 = d:AddStaticText( "Let's just get this over with. Click the OK button." )

local l4 = d:AddStaticText( "(next time hold the option key while opening this dialog to avoid it)" )

d:SetAlignment(l1, 1)

d:SetAlignment(l2, 1)

d:SetTextStyle(l4, "times", 11)

--

-- Present the dialog.

--

local result = (d:Present() == s3d.ui.Dialog.kOkay)

if result then

--

-- Store off our modified parameters.

--

parms.message = d:GetText(e2)

initialization = scriptfx.dialog.SetEmbeddedScriptParams( initialization, startParms, endParms, parms )

-- Return the name too. We don't need to store it in the shader source.

name = d:GetText(e1)

end

return result, name, initialization, functionBody

-- We always need to return the boolean result indicating an OK or cancel selection

-- along with the shader name, initialization, and function body source text.

-- Returning invalid arguments will cause the standard editor to appear.

end

CUSTOM_EDITOR@]]

-- Report what's implemented for better efficiency.

local implemented = { prepare=false, pixel=false, final=false, cleanup=true }

--

-- More useful stuff might follow here, including one time setup for our script body

-- based on the values stored in "parms"

--

Copy and paste this content into the Script Source section of a Script FX script.

-- Do something useful in here

if cleanup ~= nil then

-- I guess we're in cleanup, but we don't really care in this do nothing shader.

end

Start an edit of the Script FX shader in the usual way and you'll see the custom dialog. Hold down the option/alt key while starting the edit to revert to the standard textual script editing dialog.

Script FX Execution Phases

The Script FX shader is given several opportunities to interact with the EntityHandler object. These opportunities come during different phases of the EntityHandler traversal of the database, and in each phase one of the standard arguments to the Script FX script source is non-nil. Testing for which parameter isn't nil is how you detect which phase you are in.

  • Prepare Phase

During the prepare phase the prepare argument is non-nil. It holds a table with the keys context, handler, forGeometry, forRendering, lightShader, directionToLight, and image.

The handler key value is the EntityHandler performing the current extraction.

The forGeometry key value is a boolean used to indicate if the handler is currently extracting global values or extracting the actual geometry. This reflects the fact that the prepare phase has two distinct phases or states of its own.

1. Extracting Global Values

When a rendering starts the EntityHandler will traverse the database looking for global influences such as light sources and effect shaders. The Script FX shader is given a chance during this traversal to affect the EntityHandler state and therefore the collection of these global items. It would be a waste of time to submit geometry or surface shaders at a time like this.

2. Extracting Geometry

After the global state has been extracted from the database another traversal is made to collect the actual geometry, shaders and transformations that define the model. The Script FX shader has a chance at this point to influence the collection of this geometry. This is the phase where a shader that creates geometry would want to do its work.

The forRendering key value is a boolean that indicates whether the EntityHandler is actually performing a snapshot rendering or maybe just drawing in a modeling view or traversing the database for a modeling operation or gathering information such as a polygon count.

The lightShader and directionToLight key values are non-nil only if the Script FX shader is attached to a global directional light source. If non-nil the lightShader key value is a s3d.shade.LightShaderInfo object, while the directionToLight key value is a s3d.vec.FloatDir3d directional vector that points back toward the global directional light source origin.

The image key value is non-nil if our EntityHandler is rendering to pixels, in which case it's an s3d.image.Buffer object. This will always be the case (so far) for snapshot renderings.

  • Pixel Phase

As pixels are rendered and stuffed into the image buffers the Script FX shader has a chance to affect the stored values or to store additional values on a pixel-by-pixel basis. During this phase the pixel argument will be non-nil and hold a table with the keys lighting, state, and info. The Script FX shader is called after the standard values have already been set in the imager buffers.

The lighting key value is a function you call to gather illumination data passing the following arguments:

function (callback, worldPoint, worldNormal, worldSpotDiameter [, onlyLight])

callback -> function ( lightingResult )

Where lightingResult is an s3d.shade.LightShadingResult object passed to your callback function with the illumination data contained in it.

worldPoint -> s3d.vec.Point3d

The point in world coordinates you want to illuminate.

worldNormal -> s3d.vec.Normal3d

The normal to your world point.

worldSpotDiameter -> number

The diameter of the spot you want to illuminate.

onlyLight -> optional s3d.shade.lightShaderInfo

An optional argument that if present will restrict the lighting results to the given light source.

After calling into this function it will call back to your callback function with an s3d.shade.LightShadingResult argument for every contributing light source. You would typically accumulate values in that function for use after the lighting function returns.

The state key value is an s3d.shade.EffectPixelAssistant object. It provides access to the surface properties used to render the current pixel.

The info key value is an s3d.shade.EffectPixelInfo object. It provides access to all the standard pixel buffers as well as any that might have been added.

  • Final Phase

Once the rendering has completed the Script FX shader is given a chance to perform arbitrary post processing of the rendered image, taking advantage of the standard image buffer and any other pixel buffers or data storage it has created or knows about. During this phase the final argument will be non-nil and hold a table with the keys lighting and image.

The lighting key value is the same as for the pixel phase, documented just above here.

The image key value is an s3d.image.Buffer object containing all the pixel buffers for this rendering. This buffer is the primary reason for implementing this phase in the Script FX shader, because it allows you to make arbitrary changes to the final rendered pixels in every layer of the resulting image. You can even introduce post effects based on information you've gathered during the pixel phase and stored in Lua variables or in extra pixel layers in the image buffer itself.

  • Cleanup Phase

When the rendering is really and truly done and all effect shaders have had a shot at the final image the Script FX shader is called in this cleanup phase. This is typically where you would deallocate shared buffers used by multiple instances of a Script FX shader. By this time you want to be sure no large chunks of memory are still reserved for use by this shader. During this phase the cleanup argument will be non-nil and hold a table with the single key finished.

The finished key value is a boolean indicating whether the rendering finished successfully or not. If this value is false then the rendering either failed due to an error or was canceled by the user.

Example Script FX Shaders

The examples below provide Script FX shaders that perform their functions in one or more of the standard phases to accomplish the desired effect. It might be a good idea to start with one of these samples to create new shaders with similar functionality.

Per Pixel Effects

Th following Script FX shader will simply invert the color of any rendered pixels touching the object we are attached to. It does this by storing over the originally rendered pixel in the color layer on a pixel-by-pixel basis as the rendering proceeds.

You can download this model file or create a new Script FX and insert the following source code:

Copy and paste this content into the Initialization section of a Script FX script.

-- Put the following into the Initialization section.

-- This Script FX just inverts the color of the rendered pixels for the object it's attached to.

local implemented = { prepare=false, pixel=true, final=false, cleanup=false }

-- Report what's implemented for better efficiency.

Copy and paste this content into the Script Source section of a Script FX script.

-- Put the following into the Script Source section.

if pixel then

local info = pixel.info

local imageBuffer = info.image

local color = info.color

color.red = 1 - color.red

color.green = 1 - color.green

color.blue = 1 - color.blue

imageBuffer:SetArea( info.area, color, info.transparency, info.depth )

end

Here's where we should probably discuss what the example script is doing.

Pixel Post Effects

This next example illustrates processing a rendered image once the rendering is complete. It accesses the depth layer, computes the near and far values stored there, and then replaces the image layer contents with normalized depth information.

You can download this model file or create a new Script FX and insert the following source code:

Copy and paste this content into the Initialization section of a Script FX script.

-- This is the initialization part.

local implemented = { prepare=true, pixel=false, final=true, cleanup=false }

-- Report what's implemented for better efficiency.

Copy and paste this content into the Script Source section of a Script FX script.

-- This is the main body of the script source.

if prepare then

local image = prepare.image

if image then

local size = image:GetSize()

if size.h > 1 and size.v > 1 then

local depthLayer = image:GetDepthLayer()

if not depthLayer then

-- Avoiding some goofy trouble in the previews

image:AddLayer( s3d.image.FloatPixel.kType )

depthLayer = image:GetDepthLayer()

end

depthLayer:SetPurpose( s3d.image.Layer.kAutoThreshold )

-- Setting this will get every pixel rendered.

end

end

elseif final then

local image = final.image

if image and image:GetDepthLayer() ~= nil then

local size = image:GetSize()

local inf = 1.0e+30

local maxDepth = 0.0

local minDepth = inf

for v = 1, size.v do

for h = 1, size.h do

local thisDepth = image:GetDepth(h, v)

if thisDepth < inf then

if thisDepth > maxDepth then

maxDepth = thisDepth

end

if thisDepth < minDepth then

minDepth = thisDepth

end

end

end

end

-- console("maximum depth is %f", maxDepth)

-- console("minimum depth is %f", minDepth)

local normalizeDepth = (maxDepth > minDepth and (1.0 / (maxDepth - minDepth))) or 1.0

local newColor = s3d.image.RGBFloatPixel(0, 0, 0)

local newTransparency = 0.0

for v = 1, size.v do

for h = 1, size.h do

local newDepth = s3d.math.Clamp( minDepth, image:GetDepth(h, v), maxDepth )

local value = (newDepth - minDepth) * normalizeDepth

value = math.sqrt( value )

-- Provide more depth precision for near objects.

value = 1.0 - value

-- Reverse things so that near depths are maximum intensity?

-- Here's where you should encode the normalized depth into red green and blue.

-- Or you could leave the depth value unnormalized and maybe put the exponent

-- into one component and a higher 16-bit value in the other two?

newColor.red = value

newColor.green = value

newColor.blue = value

image:SetPixel(h, v, newColor, newTransparency, newDepth)

end

end

end

end

Procedural Geometry

Script FX shaders can introduce new geometry into the rendering pipeline. The following example shader creates an undulating surface of initially planar polygons within the bounding volume of the object this Script FX shader is attached to. The resulting geometry animates over time and is affected by all the transformations and shaders applied to our host geometry.

You can download this model file or create a new Script FX and insert the following source code:

Copy and paste this content into the Initialization section of a Script FX script.

-- This Script FX shader implements a polygonal sheet occupying the space of our host object

-- and perturbed by a configurable elevation function. The original geometry is replaced.

-- This script could be improved by adding normals and uv coordinates to the sheet.

local implemented = { prepare=true, pixel=false, final=false, cleanup=false }

-- Report what's implemented for better efficiency.

local kSurfaceFunction = "wave"

--local kSurfaceFunction = "noise"

-- Choose a surface function by uncommenting one and only one.

local kNumDivisionsU = 32

local kNumDivisionsV = 32

local kNumTrianglesPerQuad = 2

local kNumPointsPerTriangle = 3

local kNumElementsPerPoint = 3

local kTotalNumGridPoints = kNumDivisionsU * kNumDivisionsV

local kTotalNumGridElements = kTotalNumGridPoints * kNumElementsPerPoint

local kTotalNumQuads = (kNumDivisionsU - 1) * (kNumDivisionsV - 1)

local kTotalNumTriangles = kTotalNumQuads * kNumTrianglesPerQuad

local kTotalNumPoints = kTotalNumTriangles * kNumPointsPerTriangle

local kTotalNumElements = kTotalNumPoints * kNumElementsPerPoint

local effects = nil

if kSurfaceFunction == "noise" then

local kSeed = 5

local kScale = 0.25

local kTimeScale = 0.5

local kSmoothness = 1.0

local kCellSize = 1.0

local kIrregularity = 0.5

local kJaggedness = 0.5

-- effects = s3d.math.RandomField( kSeed, kScale, kTimeScale, kSmoothness )

effects = s3d.math.RandomEffects( kSeed, kScale, kTimeScale, kSmoothness, kCellSize, kIrregularity, kJaggedness )

-- The RandomEffects class provides more features but doesn't implement the Random3d

-- or Plasma3d functions properly until the 6.0.2 release.

end

local tempPt3d = s3d.vec.Point3d( 0.0, 0.0, 0.0 )

local tempExtents3d = s3d.vec.Extents3d()

local preallocatedGrid = {}

local preallocatedPoints = {}

local lastTime = nil

local lastExtent = nil

for i = 1, kTotalNumGridElements do

table.insert( preallocatedGrid, 0.0 )

end

for i = 1, kTotalNumElements do

table.insert( preallocatedPoints, 0.0 )

end

local function GetEntityExtents (time, handler)

local version = prog.scripting_version

if version.major > 1 or (version.major == 1 and (version.minor > 1 or (version.minor == 1 and version.fix >= 1))) then

return handler:GetCurrentEntity():GetExtent( time )

else

handler:GetCurrentEntity():GetExtent( time, tempExtents3d )

-- This version of the method requires a preallocated extents object which it modifies.

return tempExtents3d

-- This usage wouldn't be safe if we were making multiple nested calls to this routine.

end

end

local function ComputeElevation (time, ratioU, ratioV)

if kSurfaceFunction == "wave" then

-- A sine wave sheet.

return 0.5 + 0.5 * math.sin( (ratioU + ratioV + 2.0 * time) * math.pi )

else -- if kSurfaceFunction == "noise" then

-- A noise surface

tempPt3d.x = ratioU

tempPt3d.y = ratioV

tempPt3d.z = time

return effects:Turbulence3d( tempPt3d ) * 4.0

end

end

local function AddTriangle (points, pointIndex, grid, gridIndex)

local pointElementIndex = pointIndex * 3

local gridElementIndex = gridIndex * 3

points[ pointElementIndex + 1 ] = grid[ gridElementIndex + 1 ]

points[ pointElementIndex + 2 ] = grid[ gridElementIndex + 2 ]

points[ pointElementIndex + 3 ] = grid[ gridElementIndex + 3 ]

end

local function GetQuadTriangles (points, grid, uIndex, vIndex, quadIndex)

local stepToNextRow = kNumDivisionsU

-- local pointIndex = quadIndex * kNumTrianglesPerQuad * kNumPointsPerTriangle

local pointIndex = quadIndex * 6

-- An optimization that probably doesn't matter much.

local gridIndex = (uIndex - 1) + (vIndex - 1) * stepToNextRow

-- Stuff the first triangle of the quad.

AddTriangle( points, pointIndex, grid, gridIndex )

AddTriangle( points, pointIndex + 1, grid, gridIndex + 1 )

AddTriangle( points, pointIndex + 2, grid, gridIndex + stepToNextRow )

-- Stuff the second triangle of the quad.

AddTriangle( points, pointIndex + 3, grid, gridIndex + stepToNextRow )

AddTriangle( points, pointIndex + 4, grid, gridIndex + 1 )

AddTriangle( points, pointIndex + 5, grid, gridIndex + stepToNextRow + 1 )

end

local function GetTriangles (time, extent)

local points = preallocatedPoints

if not (lastTime and lastTime == time and lastExtent and lastExtent.low == extent.low and lastExtent.high == extent.high) then

-- No comparison operator for extents until 6.0.2

local maxCountU = kNumDivisionsU - 1

local maxCountV = kNumDivisionsV - 1

local grid = preallocatedGrid

local gridIndex = 0

do

local high = extent.high

local low = extent.low

local dx = high.x - low.x

local dy = high.y - low.y

local dz = high.z - low.z

for v = 0, maxCountV do

local ratioV = v / maxCountV

for u = 0, maxCountU do

local ratioU = u / maxCountU

grid[ gridIndex + 1 ] = low.x + ratioU * dx

grid[ gridIndex + 2 ] = low.y + ratioV * dy

grid[ gridIndex + 3 ] = low.z + ComputeElevation( time, ratioU, ratioV ) * dz

gridIndex = gridIndex + 3

end

end

end

do

local offsetV = 0

for v = 1, maxCountV do

-- console( "v:%d offsetV:%d", v, offsetV )

for u = 1, maxCountU do

local quadIndex = offsetV + u - 1

-- console( "u:%d quad index:%d", u, quadIndex )

GetQuadTriangles( points, grid, u, v, quadIndex )

end

offsetV = offsetV + maxCountU

end

end

-- console( astext(points) )

lastTime = time

lastExtent = extent

end

-- Not returning any normals or uv coordinates.

return points

end

Copy and paste this content into the Script Source section of a Script FX script.

if prepare and prepare.forGeometry then

local handler = prepare.handler

local time = handler:GetTime()

local extent = GetEntityExtents( time, handler )

-- Stuff our preallocated points table using our entity extent as a guide.

handler:HaveTriangles( GetTriangles( time, extent ) )

-- Make the existing geometry disappear.

handler:ReplaceEntity(nil)

end

A rendering of the generated geometry:

Click to preview

Here's where we should probably discuss what the example script is doing.

Time Control

Script FX shaders can control the current animation time seen by the object they are attached to. In this way you can completely take over the application of any keyframed animations in your attached object, such as reversing the animation, cycling it, or stopping it altogether.

You can download this model file or create a new Script FX and insert the following source code:

Copy and paste this content into the Initialization section of a Script FX script.

-- Put the following into the Initialization section.

-- This Script FX mucks around with time for the object it's attached to.

local implemented = { prepare=true, pixel=false, final=false, cleanup=false }

-- Report what's implemented for better efficiency.

local kAction = "repeat"

--local kAction = "cycle"

--local kAction = "reverse"

--local kAction = "faster"

-- Uncomment just one of the above values.

function GetLastKeyTime (obj)

-- Unfortunately groups and shapes don't currently respond with accurate

-- time varying information for contained objects. This might change

-- some time in the future, but for now we need to try harder to determine

-- what's animating inside groups and shapes.

console( "Processing " .. obj:GetClassName() )

if obj:IsMemberOf( "SmGroup" ) then

local lastKeyTime = 0

for index = 1, obj:GetSize() do

lastKeyTime = math.max( lastKeyTime, obj:GetIndexedInstance( index ):GetLastKeyTime() )

end

return lastKeyTime

else

return obj:GetLastKeyTime()

end

end

Copy and paste this content into the Script Source section of a Script FX script.

-- Put the following into the Script Source section.

if prepare then

local handler = prepare.handler

local time = handler:GetTime()

if kAction == "repeat" then

-- Repeat the animated keyframes over all time.

local lastKeyTime = GetLastKeyTime( handler:GetCurrentEntity() )

console( "lastKeyTime = " .. lastKeyTime )

if lastKeyTime > 0 then

time = math.mod( time, lastKeyTime )

end

elseif kAction == "cycle" then

-- Repeat the animation, but reverse the animated keyframes every other cycle time.

local lastKeyTime = GetLastKeyTime( handler:GetCurrentEntity() )

console( "lastKeyTime = " .. lastKeyTime )

if lastKeyTime > 0 then

time = math.mod( time, 2 * lastKeyTime )

if time >= lastKeyTime then

time = 2 * lastKeyTime - time

end

end

elseif kAction == "reverse" then

-- Reverse the animated keyframes in time.

local lastKeyTime = GetLastKeyTime( handler:GetCurrentEntity() )

console( "lastKeyTime = " .. lastKeyTime )

time = s3d.math.Clamp( 0, lastKeyTime - time, lastKeyTime )

elseif kAction == "faster" then

-- An arbitrary speedup.

time = time * 4

end

handler:PushTime( time )

-- Change the time seen by the object we're attached to.

end

Here's where we should probably discuss what the example script is doing.

Transformations

A Script FX shader can directly modify the transformations on the top of the EntityHandler stack for simple linear transformations or it can derive a new class from the internal Transformation class and pass an object of that class to the EntityHandler for non-linear transformation support.

The following example implements a twist transformation. You can download a prepared model file or create a new Script FX and insert the following source code:

Copy and paste this content into the Initialization section of a Script FX script.

-- This goes into the Initialization section.

local implemented = { prepare=true, pixel=false, final=false, cleanup=false }

-- Report what's implemented for better efficiency.

local kTwistAxis = s3d.vec.Point3d( 0, 1, 0 )

local kTwistScale = math.pi / 400

class 'TimeTwister' (s3d.database.Transformation)

function TimeTwister:__init (existingTransformation) super()

if existingTransformation then

-- This might be a cloning construction call itself, in which case we get no parameters.

self.existingXForm = existingTransformation:Clone()

end

self.tempQuat = s3d.vec.DoubleQuaternion( kTwistAxis.x, kTwistAxis.y, kTwistAxis.z, 0.0 )

end

function TimeTwister:GetParity ()

return true

end

function TimeTwister:GetTwist (time, pt, inverse)

-- Re-using a member variable to avoid constant garbage collection.

local tempQuat = self.tempQuat

local angle = kTwistAxis:DotProduct( pt ) * kTwistScale * time

tempQuat.r = (inverse and -angle) or angle

-- Set the 'r' (rotation) member.

return tempQuat

end

function TimeTwister:PointNormalProduct (time, pt, n, inverse)

local existing = self.existingXForm

local rotation = self:GetTwist( time, pt )

if existing then

existing:PointNormalProduct( time, pt, n, not inverse )

end

pt:Transform( rotation )

n:Transform( rotation )

if existing then

existing:PointNormalProduct( time, pt, n, inverse )

end

-- No return value for this routine. It's all done by side effect on the pt and n arguments.

end

function TimeTwister:PointProduct (time, pt, inverse)

local existing = self.existingXForm

local rotation = self:GetTwist( time, pt, inverse )

if existing then

existing:PointProduct( time, pt, not inverse )

end

pt:Transform(rotation)

if existing then

existing:PointProduct( time, pt, inverse )

end

-- Return true if the point was included in the transformation.

return true

end

Copy and paste this content into the Script Source section of a Script FX script.

-- This goes into the Script Source section.

if prepare then

local handler = prepare.handler

local newTwister = TimeTwister( handler:GetCurrentTransformation() )

handler:PushTransformation( newTwister )

end

Here's where we should probably discuss what the example script is doing.

Fun With Resource Library Items

Scripts can access the content of the resource library palette, making possible some cool features.

The following example when attached to an object named "Foo" will look for shapes in the library with names like "Foo[100]", "Foo[275]" and so on, interpreting the number within brackets as a size threshold. Then when the object with this Script FX on it falls below 275 pixels in size (measured as a rough diameter of the occupied screen area) "Foo[275]" will take the place of the original object, and when it falls below 100 pixels in size "Foo[100]" will take its place. You can create as many substitution shapes as you'd like named for different threshold sizes.

Download the sample model file or create a new Script FX shader with the following source:

Copy and paste this content into the Initialization section of a Script FX script.

local implemented = { prepare=true, pixel=false, final=false, cleanup=false }

-- Report what's implemented for better efficiency.

local function GetObject (namespace, name, index, filter)

--

-- Bottleneck this call to the namespace so we can fix a bug.

--

-- Scripting system version 1.1.1 has this method behaving correctly,

-- but until then we need to deal with a bug in some multiple return value

-- functions resulting in a duplicated return value for some Luabind object types.

--

local found, _dup_, nextIndex = namespace:GetObject( name, index, filter )

-- Discard the duplicated result if we have one. (which happens to have a bad type signature too)

-- The effect of the bug is to push the nextIndex return to position 3, but the third result

-- will be nil if the bug has been fixed.

return found, nextIndex or _dup_

end

Copy and paste this content into the Script Source section of a Script FX script.

-- We'll look for swappable shapes with the same name as the object we're attached to

-- but with an appended suffix [64], [128], and so on supplying the size threshold

-- the object must surpass for replacement in the rendering. This size is in pixels, so

-- a larger rendering will result in higher detailed versions of objects.

if prepare then

local renderer = prepare.handler:GetRenderer()

if renderer ~= nil then

-- We'll only perform our shape swapping for renderers, both in the modeling view

-- and in final snapshot renderings.

local entity = renderer:GetCurrentEntity()

local namespace = renderer:GetNamespace()

if namespace then

local name = namespace:GetName(entity)

if name then

local size = 2 * renderer:GetCurrentEntityViewRadius()

local replacement = nil

local nextObject

local nextIndex = 1

local thresholds = {}

-- Collect all object names with the selected object name prefix and a threshold index suffix.

repeat

nextObject, nextIndex = GetObject( namespace, "", nextIndex, function (obj) return obj:IsMemberOf("SmEntity") end )

-- Calling a glue function to deal with a buggy method implementation.

if nextObject then

local nextName = namespace:GetName( nextObject )

assert( nextName )

string.gsub( nextName, "^(.+)%s*%[%s*(%d+)%s*%]%s*$",

function (match, threshold)

local t = tonumber(threshold)

if t and t > 0 and match == name then

console( "Matched name %s with %s and %d", nextName, match, t )

table.append( thresholds, { t, nextObject } )

end

end

)

end

until not nextObject

-- Ensure the threshold values are sorted.

if table.getn( thresholds ) > 1 then

table.sort( thresholds, function (a, b) return a[1] < b[1] end )

end

-- Find the smallest threshold value greater than our size

for _, threshold in ipairs( thresholds ) do

if size < threshold[1] then

replacement = threshold[2]

break

end

end

if replacement then

renderer:ReplaceEntity(replacement)

end

thresholds = nil

end

end

end

end

Here's where we should probably discuss what the example script is doing.