All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Creating a custom modifier POP


POPs are Particle Operators and are used to modify particle systems. To understand how to write a custom POP, it is best to study a simple example.

This example POP will change the particle's color to be brighter the closer it is to a point in 3D space. It is a simple example of a modifier POP that changes a particle's attributes. It will demonstrate the general structure of a POP.

Anatomy of a modifier POP

This example (.h / .C) can be found in the toolkit samples.

#include <UT/UT_Color.h>
#include <GEO/GEO_Point.h>
#include <GU/GU_Detail.h>
#include <OP/OP_Operator.h>
#include "POP_SpotLight.h"

The UT_Color class is needed because it will be used to convert values between different color spaces. Aside from that, all of these header files are required to compile a custom POP.

static PRM_Name names[] =
PRM_Name("center", "Center"),
POP_SpotLight::myTemplateList[] =
PRM_Template(PRM_XYZ_J, 3, &names[0]),

Most POPs have an activation field and a source group parameter. The activation field is used to turn the entire POP off and on. If the parameter evaluates to 0 or less, nothing is processed. The source group allows the user to apply the POP only to a specific group of points. Although these fields are not absolute requirements, it may be a good idea to follow this style as it is already used by most other POPs.

The third parameter represents the position in space that will be used as a center of brightness.

POP_SpotLight::myTemplatePair (myTemplateList, &POP_LocalVar::myTemplatePair);
POP_SpotLight::myVariablePair (0, &POP_LocalVar::myVariablePair);

There are a lot of local variables built into POPs that represent particle attributes. These are handled by the POP_LocalVar class. In order to use them, they must be "inherited" using the template and variable pair methodology. Again, it is not imperative that a custom POP inherit from the POP_LocalVar class (They can inherit from POP_Node instead). However, most modifier POPs will be far more useful if they take advantage of the local variables already defined in POPs.

new OP_Operator("hdk_spotlight", // Name
"SpotLight", // English
POP_SpotLight::myConstructor, // "Constructor"
&POP_SpotLight::myTemplatePair, // simple parms
1, // MinSources
1, // MaxSources
&POP_SpotLight::myVariablePair));// variables

This new operator must be added to the operator table.

POP_SpotLight::myConstructor (OP_Network* net, const char* name,
OP_Operator* entry)
return new POP_SpotLight(net, name, entry);
int *POP_SpotLight::myIndirect = 0;
POP_SpotLight::POP_SpotLight (OP_Network* net, const char* name,
OP_Operator* entry)
:POP_LocalVar (net, name, entry)
if (myIndirect)
myIndirect = allocIndirect(sizeof(myTemplateList)/sizeof(PRM_Template));
POP_SpotLight::~POP_SpotLight (void)

The POP doesn't not hold any data that needs to be created and destroyed in the constructors and destructors. Every POP ultimately is a POP_Node, but note that this POP inherits from the POP_LocalVar class. The POP_LocalVar class in turn inherits from POP_Node.

The call to allocIndirect() creates an array of values used to store parameter indices. These indices are used during parameter evaluation. Note that myIndirect is a static member shared among all instances, allocated only once when the first instance of POP_SpotLight is created. Such caching of parameter indices is not strictly necessary, but for POPs, where parameters are often evaluated per particle, eliminating the overhead of parameter lookup is useful.

POP_SpotLight::cookPop (OP_Context& context)
POP_ContextData* data = (POP_ContextData*) context.getData();

The cookPop() method is the heart and soul of a POP. It is here that the POP modifies the particle system passed into it. Unlike other operator types, POPs themselves don't contain any data. Instead, the geometry detail to modify is passed in through the context. The detail represents the state of the particle system. So, it is the job of the POP to take a state (or detail) and update it for the current time.

All of the POPs specific information is passed into the cook method via a POP_ContextData object attached as data to the supplied OP_Context. The POP_ContextData holds a lot of persistent information used to cook the particle system. This includes the detail itself, the offsets of many attributes, the transform object, and the time increment.

float t = context.getTime();
int thread = context.getThread();
GEO_ParticleVertex* pvtx;
GEO_Point* ppt;
POP_FParam centerx;
POP_FParam centery;
POP_FParam centerz;

Variables are allowed in most POP fields. For the sake of efficiency, if these variables are not dependent on local variables and thus do not change per-particle, they shouldn't still be re-evaluated over and over again. To solve this, parameter values are cached if they don't change per particle. The POP_FParam type is actually just a function pointer. If the parameter is variable dependent, this function will be the function usually used to evaluate the parameter. Otherwise, it will be a function that just returns a cached value. There are macros that help set up the function pointers further down. These macros expect the cook time and current thread to be available as t and thread, respectively.

UT_String sourceName;
GA_PointGroup* sourceGroup = NULL;
if (lockInputs(context) >= UT_ERROR_ABORT)

The inputs should be locked. This ensures that all POPs that this POP is dependent on are cooked.


Right at the beginning of the cook, dynamic variables need to be set up in the POP_LocalVar class. This will enable all user defined variables to be used when evaluating parameters during the cook.

if (buildParticleList(context) >= UT_ERROR_ABORT)
goto done;

Each POP needs to know which particles to act on. This is usually controlled by two factors: the input wires and an input group. Each wire that goes into a POP represents a set of particle primitives and this list is stored in a particle list. A call to the POP_Node method buildParticleList() builds this list. The list is stored in the POP_Node variable, myParticleList.

if (data->isGuideOnly())
goto done;

A POP node's guide geometry is typically updated as part of the cook when a call to POP_Node::doUpdateViewport(POP_ContextData *) (not seen in this example) returns true. In certain situations, Houdini will want to update only a POP node's guide geometry. In these situations, the cookPop() method will still be called, but data->isGuideOnly() will return true. This example POP doesn't have any guide geometry, so we can short circuit the cooking if only the guide geometry is requested.

if (!checkActivation(data, (POP_FParam) ACTIVATE))
goto done;

The activation field determines whether the POP is active or not. If it isn't, the processing can be short circuited before any more work is done. The checkActivation() method will take care of allowing certain local variables in the field.

if (sourceName.isstring())
sourceGroup = parsePointGroups((const char*) sourceName,
if (!sourceGroup)
addError(POP_BAD_GROUP, sourceName);
goto done;

As stated earlier, the POP can be restricted to act on only a subset of the particles available. This group of particles (points) is built using the parsePointGroups() method of POP_Node.

setupVars(data, sourceGroup);

The setupVars() method found in POP_LocalVar needs to be called to set up attribute offsets that are used when evaluating local variables. This call must occur before evaluating any parameters that might make use of these local variables.

POP_FCACHE(centerx, CENTERX, getCenterX, myCenterX, POP_SpotLight);
POP_FCACHE(centery, CENTERY, getCenterY, myCenterY, POP_SpotLight);
POP_FCACHE(centerz, CENTERZ, getCenterZ, myCenterZ, POP_SpotLight);

POP_FCACHE is one of the macros that are used to set up the parameter evaluation function pointer. If the parameter is variable dependent, the macro sets the function pointer to the regular evaluation function which in this case is CENTERX (defined in POP_SpotLight.h). Otherwise, it evaluates the parameter once, stores it in a cache variable and the function pointer used is an internal method that just returns the cached value. This internal method and cache value are set up in the class definition by using the POP_FPARM macro.

myCurrIter = 0;

The POP_LocalVar class uses two variables to determine which iteration and which point is currently being processed: POP_LocalVar::myCurrIter and POP_LocalVar::myCurrPt respectively. For the class to properly evaluate local variables, these two values must be updated as the points are processed.

if (sourceGroup)
FOR_ALL_GROUP_POINTS(data->getDetail(), sourceGroup, myCurrPt)
changePoint(myCurrPt, data, t, centerx, centery, centerz);
for (part = myParticleList.iterateInit() ;
part ; part = myParticleList.iterateNext())
for (pvtx = part->iterateInit() ; pvtx ; pvtx = pvtx->next)
myCurrPt = pvtx->getPt();
changePoint(myCurrPt, data, t, centerx, centery, centerz);

The easiest way to process particles is to have a single method that modifies the particle. Here, this method is called changePoint(). This method is then applied to the point group if a source group is being used or to the particle list otherwise.


It is useful to have a label to allow jumping directly to the clean up code at the end of the cook.


We don't want any lingering references to user-defined variables outside of the cook code path.


The inputs were locked at the beginning of the function. They must be unlocked before exiting.

myCurrPt = NULL;

Parameters are sometimes evaluated outside of the cook code resulting in attempts to resolve local variables. We need to make sure such evaluations don't reference an obsolete pointers, so myCurrPt should be reset.

return error();

The cookPop() method returns the error state of the node, so this is generally the last statement.

POP_SpotLight::changePoint (GEO_Point* ppt, POP_ContextData* data, float t,
POP_FParam centerx, POP_FParam centery,
POP_FParam centerz)
UT_Vector3 center;
float r, g, b;
float h, s, v;
float d2;

The purpose of this function is to modify the passed in point by changing its color.

center.assign(POP_PEVAL(centerx), POP_PEVAL(centery), POP_PEVAL(centerz));

Find out the center position. The POP_PEVAL macro evaluates the function pointer.

p = ppt->getPos();
d = p - center;
UT_Vector3 color = ppt->getValue<UT_Vector3>(data->getDiffuseOffset());

The POP_ContextData holds the attribute offsets for most attributes used by POPs.

RGBtoHSV.setValue(color.x(), color.y(), color.z());
RGBtoHSV.getHSV(&h, &s, &v);
d2 = d.length2();
v = UTequalZero(d2) ? 1.0f : 1.0f / d.length2();
if (v > 1.0f)
v = 1.0f;
HSVtoRGB.setValue(h, s, v);
HSVtoRGB.getRGB(&r, &g, &b);
color.assign(r, g, b);
ppt->setValue<UT_Vector3>(data->getDiffuseOffset(), color);

The UT_Color class is used to convert the particle's color to HSV space, modify the intensity based on how far the particle is from the center position and change it back to RGB. Finally, the attribute is updated.

POP_SpotLight::addAttrib (void* userdata)
POP_ContextData* data = (POP_ContextData*) userdata;

It is fairly common that a POP will need to introduce a new point attribute to the particle system. Typically this is done by the virtual addAttrib() method called when a particle system is reset, as well as when a change is detected in the POP network responsible for cooking a particle system. This example POP will modify the diffuse color attribute of the particle. The function addDiffuseAttrib() will ensure that this point attribute exists. The POP_Node base class contains many methods to add various point attributes.


This example demonstrated the general structure of a modifier POP. It covered the following topics which are common to many POPs:

  • Having an activation field to turn POPs on and off.
  • Modifying only particles in a source group.
  • Local variables from the POP_LocalVar class.
  • The list of particle systems to modify held in myParticleList.
  • Using the POP_ContextData to retrieve attribute offsets.