Writing Plugins for Art of Illusion
Copyright 2000-2014 by Peter Eastman
Author: Peter Eastman
Last Modified: Jan. 18, 2014
Modified For: v3.0
Original Release Date: Oct. 4, 2000
Originally Written For: v0.6
Contents
- Overview
- Types of Plugins
- Packaging Plugins
- The XML File
- Plugin Processing
- Resources
- Using Plugins in Scripts
Overview
Art of Illusion is based around a plugin architecture that allows it to be extended in many ways. Plugins typically:
- Add new features which are not part of the core program
- Are written in Java
- May contain any number of Java classes, data files, and other resources
- Are packaged as a single jar file
- Are installed simply by placing the jar file in the "Plugins" folder
- May themselves define interfaces so other plugins can extend them in turn
More precisely, a plugin is a jar file containing one or more Java classes that implement
known interfaces or extend known classes. The core application defines many such interfaces and
classes (known as plugin types), but plugins can also define their own types.
The jar file also contains an XML file called "extensions.xml". This file lists all
the plugin classes found in that jar file. When Art of Illusion starts up, it looks at
every jar file in its Plugins directory, reads the extensions.xml file in each one,
and registers the plugin classes.
Types of Plugins
The following plugin types are defined by the core application:
- Renderers create images of scenes. Art of Illusion is distributed with two
renderers (the Raytracer and the Raster renderer), which are packaged together as a single plugin.
Renderers are Java classes which implement the artofillusion.Renderer interface.
- Modelling Tools provide new capabilities for creating or editing parts of a scene.
They are listed in the Tools menu. Modelling Tools are Java classes which implement the
artofillusion.ModellingTool interface.
- Translators allow Art of Illusion to read and write scenes in foreign file formats.
Translators are Java classes which implement the artofillusion.Translator interface.
- Textures allow you to define procedural textures which you can use in your scenes.
Textures are Java classes which subclass artofillusion.texture.Texture. (In practice,
they generally subclass either artofillusion.texture.Texture2D or artofillusion.texture.Texture3D.)
- Texture Mappings define how a texture is mapped to the surface of an object. Texture mappings
are Java classes which subclass artofillusion.texture.TextureMapping. (In practice, new mappings defined by
plugins will usually subclass artofillusion.texture.NonlinearMapping2D.)
- Materials allow you to define procedural materials which you can use in your scenes.
Materials are Java classes which subclass artofillusion.material.Material. (In practice, they generally subclass
artofillusion.material.Material3D.)
- Material Mappings define how a material is mapped to the interior of an object. Material mappings
are Java classes which subclass artofillusion.material.MaterialMapping.
- Image Filters define post-rendering effects that can be applied to images. Any number
of filters may be attached to a camera, and they may define adjustable parameters so that the effect
of the filter can be modified or even animated. Image filters are Java classes which subclass
artofillusion.image.filter.ImageFilter.
- Procedural Modules add new functions to the Procedure editor. They can be used within
any procedural element of a scene: textures, materials, animation tracks, etc. Procedural modules
are Java classes which subclass artofillusion.procedural.Module.
- Generic Plugins provide a general purpose mechanism for implementing new features
that do not fit easily into any of the above categories. They are Java classes which implement
the artofillusion.Plugin interface. This interface defines a single method, processMessage(), which
is called when various events take place: the program is started, the program exits, a scene
editing window is opened, a scene editing window is closed, a file is saved, etc.
In addition to the plugin types defined by the core application, the Raytracer (which is itself
a plugin) defines two plugin types:
- Raytracer object factories allow the raytracer to directly render new types of objects.
They implement the artofillusion.raytracer.RTObjectFactory interface.
- Photon source factories allow new types of objects to generate photons when building
photon maps. They implement the PhotonSourceFactory interface.
For details on how to write plugins of each of these types, see the API documentation for their
respectives classes and interfaces. There also is a separate tutorial specifically on writing
Texture and Material plugins. Be aware that plugin objects get instantiated by calling
newInstance() on the class object. This means that plugin classes always need to have a constructor
that takes no arguments.
Packaging Plugins
To package your Java classes as a plugin, place the main plugin class (the one which implements
the appropriate interface or extends the appropriate class) and any other classes or files it
needs into a single jar file. You may include multiple plugins in a single jar file. In addition, the jar
file must contain an XML file called "extensions.xml" (all lower case) that provides information
about the contents of the jar file.
Each class file must be stored in the correct directory within the jar file that
corresponds to the package it is in. Otherwise, the class loader will not be able to locate it.
To make all of this clearer, suppose that you wish to package two plugins into a jar file, whose
fully qualified class names are com.mycompany.GoodPlugin and com.mycompany.BetterPlugin. The
jar file should then contain three entries (in addition to any other classes or files which may be
used by the plugins):
com/mycompany/GoodPlugin.class
com/mycompany/BetterPlugin.class
extensions.xml
The content of the extensions.xml file will look something like this:
<?xml version="1.0" standalone="yes"?>
<extension name="AmazingPlugins" version="1.0">
<author>Peter Eastman</author>
<date>Oct. 15, 1842</date>
<description>Plugins that will totally change your life.</description>
<plugin class="com.mycompany.GoodPlugin"/>
<plugin class="com.mycompany.BetterPlugin"/>
</extension>
The key part of this file is the two <plugin> tags. Each one identifies a plugin
class contained in the jar file. Most of the other content, such as the <author>,
<date>, and <description> tags are just for information. They are shown to
the user in the Scripts and Plugins Manager window but have no other effect on the
behavior of the plugin.
Plugins can also define new plugin types by which they can themselves be extended. To define
a new plugin type, include a <category> tag in extensions.xml:
<category class="com.mycompany.AmazingExtension"/>
The value of the "class" attribute may be a class or interface. Other plugins may then extend
the class or implement the interface. This is discussed in more detail below.
By default, every plugin jar file is loaded with an independent classloader, so the classes
defined in one cannot "see" the classes defined in any other. Usually this is exactly what
you want. It eliminates the risk of class name conflicts between plugins, and reduces the
possibilities for one plugin to interfere with the operation of another one. But sometimes
you need plugins to interact more directly. For example, you might want the code in one jar
file to directly use some classes that were defined in another one. Also, if a jar file
defines a new plugin type, other jar files must be able to implement the corresponding interface
or subclass the corresponding class.
You can do this by including an <import> tag in extensions.xml:
<import name="Renderers"/>
The value of the "name" attribute is the name of the plugin to import, as specified by its
extensions.xml file. In this case we are importing the Renderers plugin that comes bundled
with Art of Illusion. This would allow you, for example, to define a raytracer object factory
by implementing RTObjectFactory. Each plugin is still loaded with its own classloader, but
the classloader for the imported plugin becomes a parent of the one for the plugin that imports
it.
Importing a plugin completely breaks down the barriers between plugins, so you lose all the
protections you normally get from plugin isolation. If you only need limited
interaction between them, an alternative is to export only the particular methods that should be
callable from other plugins. Here is an example taken from the Tools plugin bundled with
Art of Illusion:
<plugin class="artofillusion.tools.LatheTool">
<export method="latheCurve" id="artofillusion.tools.LatheTool.latheCurve"/>
</plugin>
The class artofillusion.tools.LatheTool has a static method latheCurve().
This method is exported so it can be called by any other plugin without needing to
import the whole plugin. This is discussed more below.
When you start up Art of Illusion, one of the first things it does is to look through every file
in the Plugins directory. For each one, it first determines whether it is a zip or jar file, and
if so, looks for an entry called "extensions.xml". It loads that entry and uses it to identify
the main plugin classes for that file. It loads each plugin class, then creates an instance of
it by calling newInstance() on the class object. For some plugin types, such as Renderers, only
that one instance is ever created and used repeatedly. For other types, such as Textures, many
other instances may be created later.
This procedure has a few consequences which you should keep in mind. First, it means that every
plugin class must have a constructor that takes no arguments . This allows it to be instantiated
with a call to Class.newInstance().
Second, when many plugins are present, the time required to load and instantiate all of them can
be significant. It also means that memory must be used to store every plugin class, even if the
user never actually uses that plugin. To alleviate both of these problems, it often is a good
idea to minimize the size of the main plugin class. By placing the bulk of the code into helper
classes which do not get loaded until the plugin is actually used, you can save memory and reduce
the startup time.
All this processing is done by the class artofillusion.PluginRegistry. It provides methods
you can use to interact with plugins from your own code.
In one of the examples above, we defined a new plugin type com.mycompany.AmazingExtension.
Here is how you can retrieve a list of every registered plugin of that type:
List<AmazingExtension> extensions = PluginRegistry.getPlugins(AmazingExtension.class);
You can then make use of them in whatever way is appropriate for your code. It also allows you
to invoke methods that were exported by plugins. Here is how you would invoke the latheCurve()
method that was exported by LatheTool:
Mesh mesh = (Mesh) PluginRegistry.invokeExportedMethod("artofillusion.tools.LatheTool.latheCurve", curve, 3, 8, 360.0, 0.0)
The first argument is the id that was specified in the <export> tag. Any additional arguments
are simply passed on to the method.
Plugins can extend Art of Illusion in two ways. First, they can provide new executable code. That
is how all the plugin types discussed above work. Second, they can provide new data. This is done
by providing resources.
A resource typically represents a data file stored in the plugin jar. It is defined by the following
information:
- A type that indicates what type of resource this is (i.e. what the file is used for)
- An ID used to identify this particular resource
- Optionally a locale, allowing there to be multiple localized versions of a resource (which
might come from multiple plugins)
- The name of the file containing the data (or more precisely, the string to pass to
the getResource() method of the plugin's classloader)
PluginRegistry provides static methods for accessing resources. Call getResources()
to get a list of all resources of a given type, and getResource() to access the resource with
a particular ID and type. See the API documentation for details.
To define a resource, include a <resource> tag in your extensions.xml file. For example,
<resource type="TranslateBundle" id="amazingPlugin" name="amazing"/>
If you want to include multiple versions of a resource for different locales, use a separate
<resource> tag for each one and include an attribute specifying the code for the language,
or optionally, language and country: locale="es" or locale="en_GB".
There are two standard resource types used by Art of Illusion: translate bundles and UI themes.
Translate bundles are used to provide localizable text. They have the type code "TranslateBundle".
See the API documentation for artofillusion.ui.Translate for details. UI themes are used
to define icons and other information that can be customized by the user. They have the type code
"UITheme". See the API documentation for artofillusion.ui.ThemeManager for details.
Of course, you can also define entirely new resource types. Just select a value for the "type"
attribute, then use PluginRegistry to look up resources with that type code.
When a Groovy or Beanshell script is executed, it uses a specially constructed classloader that
includes all classloaders for all plugins as parents. This means that scripts can
directly access any class defined in any plugin with no special effort.
Although the script can see classes in different plugins, those classes still
cannot see each other. So although this does create a danger of class name conflicts, it is
fairly small. If a script tries to invoke a class, and two different plugins each define a
class with that name, it is unpredictable which one it will get. But if a plugin class invokes
another class in the same jar, it is always guaranteed to get the right one, even if another jar
happens to define a class with the same name. It is only the script itself that can see everything,
not plugin code invoked by the script.