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.