exppad* / blog / Writing a custom modifier for Blender July 18, 2019

Modifiers are procedural geometry effects applied dynamically to meshes, enabling flexible non-destructive workflows. This is a programming guide for adding new modifiers to Blender 2.80.

Custom modifiers cannot be written in Python, so this guide will show what parts of the C code of Blender must be modified.

Building Blender

Since we are going to modify the core of Blender, we first need to build Blender from source. This process is well documented in Blender's Wiki: Building Blender. I will assume that you followed these instruction regarding directory names.

Before actually starting our changes, it is recommended to create a branch to isolate our changes from upstream development. Let's call it for example pizza-modifier:

/../blender-git/blender $ git checkout -b pizza-modifier

Throughout this post, we will add a "Pizza" modifier. I don't know what it can do, but at least we are sure not to get confused by other occurrences of the word "pizza" in the source base.

Overview

So, what do we have to change? And why do we need to change so many files? Obviously, we must at least code the behavior of the modifier itself. But we must also ensure that it is listed in the UI, that is can be accessed through the Python API, and that its parameters are saved in blend files. The last points are achieved using the DNA/RNA mechanism present at the heart of Blender's architecture.

Most of our changes will be located in the directory /../blender-git/blender/source/blender/modifiers/.

Core modifier

Creating a modifier means defining a global variable of type ModifierTypeInfo. We'll then register it in the global list of modifiers. To match the coding style and play nicely with the existing macro, this variable must be called modifierType_Pizza (where "Pizza" is your actual modifier name, capitalized).

Modifier Type Info

The modifier type info variable is typically defined in a file called MOD_pizza.c located in /../blender-git/blender/source/blender/modifiers/intern. So, create this file, and add it to the list of files to build in the CMakeLists.txt of the parent directory (together with all other lines starting with intern/MOD_).

At the very least, your MOD_pizza.c file must contain:

#include "BKE_modifier.h"

ModifierTypeInfo modifierType_Pizza = {
    /* name */ "Pizza",
    /* structName */ "PizzaModifierData",
    /* structSize */ sizeof(PizzaModifierData),
    /* type */ eModifierTypeType_None,
    /* flags */ 0,

    /* copyData */ NULL,

    /* deformVerts */ NULL,
    /* deformMatrices */ NULL,
    /* deformVertsEM */ NULL,
    /* deformMatricesEM */ NULL,
    /* applyModifier */ NULL,

    /* initData */ NULL,
    /* requiredDataMask */ NULL,
    /* freeData */ NULL,
    /* isDisabled */ NULL,
    /* updateDepsgraph */ NULL,
    /* dependsOnTime */ NULL,
    /* dependsOnNormals */ NULL,
    /* foreachObjectLink */ NULL,
    /* foreachIDLink */ NULL,
    /* foreachTexLink */ NULL,
    /* freeRuntimeData */ NULL,
};

The struct ModifierTypeInfo is defined in source/blender/blenkernel/BKE_modifier.h (at line 139 at the time I write this, i.e. with Blender 2.80 RC). The meaning of the different fields is well explained in this header file, so you should definitely take a look.

I will focus on some essential fields and leave the others to your exploration, because depending on what your modifier does you'll not need all of them.

Type and Flags

The first field to set (after the name) is the modifier's type. This is an enum defined at the beginning of BKE_modifier.h, so once again go and take a look at the options to find the best match for your modifier. If you are not sure, you can try and look at the MOD_something.c file of an existing modifier that has a similar behavior.

Another important field is flags. Again, an enum defined and documented in BKE_modifier.h. It is likely that you will use eModifierTypeFlag_AcceptsMesh to tell Blender that your modifier processes mesh data. You can set several flags using the logical or operator (|).

Apply Modifier

The heart of the modifier is the function you set in applyModifier. Before defining the modifier type info variable, define a function like:

static Mesh *pizza_applyModifier(struct ModifierData *md,
                                 const struct ModifierEvalContext *ctx,
                                 struct Mesh *mesh)
{
  return mesh;
}

and set the applyModifier field to pizza_applyModifier (or whatever name you gave to the function above). This function will be where all your processing occurs. It is totally up to you, and the returned mesh can be a new mesh, allocated using functions from source/blender/blenkernel/BKE_mesh.h. See example in appendix.

Other fields

For some fields, you can use generic utility functions defined in BKE_modifier.h. For instance, you can set copyData to modifier_copyData_generic.

DNA and RNA

There are two very important fields in the ModifierTypeInfo I did not mention yet. Those are structName and structSize. They refer to the structure PizzaModifierData that will be the DNA of our modifier.

The DNA is what gets saved on disc in the .blend file. It must contain all the saved parameters. Following the biologic metaphor, to each DNA is associated an RNA struct. This one is used at runtime only, to edit, transfer, undo, redo, etc. the data related to the modifier.

DNA

All the DNA structs used in Blender are defined in the source/blender/makesdna/ directory. For modifiers, it is in file DNA_modifier_types.h

First thing to change: add eModifierType_Pizza to the ModifierType enumeration at the beginning of the file, just before NUM_MODIFIER_TYPES. This enum must NOT be reordered because the enum values are what gets saved in blend files so it would break backward compatibility. This is also the reason whay enum values are explicitly stated.

At the end of the file, we add the DNA struct for our modifier. Its very first field MUST be of type ModifierData. It is an inheriting mechanism, making it possible to use a pointer to the struct as a more generic ModifierData*.

typedef struct PizzaModifierData {
  ModifierData modifier;
  int num_olives;
  int _pad0;
} PizzaModifierData;

In this example, our modifier has one parameter, an integer called num_olives.

Another requirement is to pad the struct to make its overall size be a multiple of 8 bytes. If it is not naturally the case, add fields extra starting with _pad to reach a multiple of 8.

Be aware that this whole file is processed by the special makesdna program executed before building Blender, so no fancy stuff nor preprocessor macros must be used here.

NB: Since we are referring to the the DNA struct in the modifier type info, we must include DNA_modifier_types.h in your MOD_pizza.c file.

RNA

The RNA mechanism is located in source/blender/makesrna/. Similarly to DNA, the files in intern are preprocessed before building by a custom program called makesrna. This frees us from implementing some very repetitive tasks regarding DNA/RNA conversion.

Modifier data

The RNA for modifiers is defined in makesrna/intern/rna_modifier.c. Unless you are making advanced stuff with DNA, you will mainly have to write a simple function rna_def_modifier_pizza similar to all the other ones already present. Add it just before void RNA_def_modifier:

static void rna_def_modifier_pizza(BlenderRNA *brna)
{
  StructRNA *srna;
  PropertyRNA *prop;

  // Define the RNA and bind it to the PizzaModifierData DNA struct
  srna = RNA_def_struct(brna, "PizzaModifier", "Modifier");
  RNA_def_struct_ui_text(srna, "Pizza Modifier", "");
  RNA_def_struct_sdna(srna, "PizzaModifierData");
  RNA_def_struct_ui_icon(srna, ICON_MOD_ARRAY);

  // There will be such a block for each data field of PizzaModifierData
  prop = RNA_def_property(srna, "num_olives", PROP_INT, PROP_NONE);
  RNA_def_property_range(prop, 0, 100);
  RNA_def_property_ui_range(prop, 0, 100, 1, -1);
  RNA_def_property_ui_text(prop,
                           "Olives",
                           "The number of olives on the pizza");
  RNA_def_property_update(prop, 0, "rna_Modifier_update");
}

NB: you can chose a custom icon for your RNA instead of the ICON_MOD_ARRAY I chose here.

At the very end of RNA_def_modifier(), add a call to the function we just defined: rna_def_modifier_pizza(brna);

Modifier type enum

Since we modified the DNA of the ModifierType enumeration, we need to also edit its RNA. To do so, at the beginning of rna_modifier.c add an entry to rna_enum_object_modifier_type_items, in the appropriate section. Sections headers are lines of type {0, "", 0, N_("Modify"), ""},. They directly relates to the drop down displayed in the UI when adding a new modifier.

We must also edit the function rna_Modifier_refine to add before /* Default */:

case eModifierType_Pizza:
  return &RNA_PizzaModifier;

NB: The variable RNA_PizzaModifier does not need to be defined by us, it will be generated by makesrna.

So, we are done with the DNA/RNA related changes. You might have some more advanced needs regarding modifier parameters. If so, feel free to look at how other modifiers handle special cases, they are all defined in the same file.

NB: For more details about RNA, you can check out this archive: https://archive.blender.org/wiki/index.php/Dev:2.5/Source/Architecture/RNA/

Final steps

Our modifier has a core, is stored in blend files thanks to DNA and transmitted through RNA, but we still need to register it in some other places.

Modifier table

The modifier's type info needs to be registered in the global modifier table. This is done by adding at the end of source/blender/modifiers/MOD_modifiertypes.h, before the prototype of modifier_type_init, the following line:

extern ModifierTypeInfo modifierType_Pizza;

and in source/blender/modifiers/intern/MOD_util.c, before #undef INIT_TYPE:

INIT_TYPE(Pizza);

Outliner

The outliner is the UI space that can list all the technical details about the blend file currently opened. It must hence know about all modifiers, including our new Pizza modifier. Edit in source/blender/editors/space_outliner/outliner_draw.c the function tree_element_get_icon to add an appropriate case eModifierType_Pizza. (I don't know why it cannot get this piece of information from the RNA, though.)

Conclusion

You now have a first skeleton for your modifier. From now, you will be able to learn a lot by reading the comments of BKE_modifier.h and more importantly other modifier's implementation.

This is intended to be a comprehensive guide, so if you find it somehow not clear, to quick or on the contrary too verbose about something, feel free to give me some feedback on twitter!

I will soon come back with another way to write custom modifiers for Blender. A way that does not need to build Blender because it uses a runtime plug-in standard: Open Mesh Effects. At the moment, it is still under development, but stay tuned!

Appendix: Example of applyModifier

At this point, you should have a working modifier, and the project should build correctly. But the modifier does nothing, so just to give a simple example, here is what the applyModifier function could look like, creating a simple plan whose width is driven by the num_olives parameter:

#include "BKE_mesh.h"

static Mesh *applyModifier(struct ModifierData *md,
                           const struct ModifierEvalContext *ctx,
                           struct Mesh *mesh)
{
// Convert the generic ModifierData to our modifier's DNA data.
// This is ensured to be valid by the architecture.
  PizzaModifierData *pmd = (PizzaModifierData *)md;

  Mesh result = BKE_mesh_new_nomain(4 /* vertices */, 0, 0, 4 /* loops */, 1 /* face */);

  // Fill coordinates of the 4 vertices
  result->mvert[0].co[0] = -1.f;
  result->mvert[0].co[1] = -pmd->num_olives;
  result->mvert[0].co[2] = 0.f;

  result->mvert[1].co[0] = -1.f;
  result->mvert[1].co[1] = pmd->num_olives;
  result->mvert[1].co[2] = 0.f;

  result->mvert[2].co[0] = 1.f;
  result->mvert[2].co[1] = pmd->num_olives;
  result->mvert[2].co[2] = 0.f;

  result->mvert[3].co[0] = 1.f;
  result->mvert[3].co[1] = -pmd->num_olives;
  result->mvert[3].co[2] = 0.f;

  // Fill the loops 
  result->mloop[0].v = 0;
  result->mloop[1].v = 1;
  result->mloop[2].v = 2;
  result->mloop[3].v = 3;

  // Fill the face info, i.e. its first loop and total number of loops
  result->mpoly[0].loopstart = 0;
  result->mpoly[0].totloop = 4;

  // Fill edge data automatically
  BKE_mesh_calc_edges(result, true, false);

  return result;
}