Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukas Tönne <lukas.toenne@gmail.com>2015-01-15 13:51:30 +0300
committerLukas Tönne <lukas.toenne@gmail.com>2015-01-20 11:30:11 +0300
commit91b70d3c56cb863b9c6184a726b35d32e5f4480c (patch)
tree0e47bc6aeefee37eb152b1607f2c6879d6b14d83
parent8f9f55498e1341c9bda4ba61d4f4e8fa33c022d2 (diff)
New operator for copying (hair) particle systems from one object to
another, including edit data (grooming). This uses basically the same method as the existing connect/disconnect feature. The main difference is that it allows working with multiple objects and transferring the //particle/hair data// instead of the //mesh// data (which is what connect/disconnect expects). This is a much more realistic workflow when rigging, topology etc. changes and groomed hair has to be transferred to the changed model.
-rw-r--r--source/blender/blenkernel/intern/particle.c1
-rw-r--r--source/blender/editors/physics/particle_edit.c18
-rw-r--r--source/blender/editors/physics/particle_object.c366
-rw-r--r--source/blender/editors/physics/physics_intern.h1
-rw-r--r--source/blender/editors/physics/physics_ops.c1
5 files changed, 323 insertions, 64 deletions
diff --git a/source/blender/blenkernel/intern/particle.c b/source/blender/blenkernel/intern/particle.c
index 499475104e6..72cb69aa183 100644
--- a/source/blender/blenkernel/intern/particle.c
+++ b/source/blender/blenkernel/intern/particle.c
@@ -3041,6 +3041,7 @@ void object_remove_particle_system(Scene *UNUSED(scene), Object *ob)
DAG_relations_tag_update(G.main);
DAG_id_tag_update(&ob->id, OB_RECALC_DATA);
}
+
static void default_particle_settings(ParticleSettings *part)
{
part->type = PART_EMITTER;
diff --git a/source/blender/editors/physics/particle_edit.c b/source/blender/editors/physics/particle_edit.c
index 3d979562434..29b9db2a63c 100644
--- a/source/blender/editors/physics/particle_edit.c
+++ b/source/blender/editors/physics/particle_edit.c
@@ -83,9 +83,11 @@
#include "physics_intern.h"
-static void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys);
-static void PTCacheUndo_clear(PTCacheEdit *edit);
-static void recalc_emitter_field(Object *ob, ParticleSystem *psys);
+void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys);
+void PTCacheUndo_clear(PTCacheEdit *edit);
+void recalc_lengths(PTCacheEdit *edit);
+void recalc_emitter_field(Object *ob, ParticleSystem *psys);
+void update_world_cos(Object *ob, PTCacheEdit *edit);
#define KEY_K PTCacheEditKey *key; int k
#define POINT_P PTCacheEditPoint *point; int p
@@ -1074,7 +1076,7 @@ static void pe_iterate_lengths(Scene *scene, PTCacheEdit *edit)
}
}
/* set current distances to be kept between neighbouting keys */
-static void recalc_lengths(PTCacheEdit *edit)
+void recalc_lengths(PTCacheEdit *edit)
{
POINT_P; KEY_K;
@@ -1090,7 +1092,7 @@ static void recalc_lengths(PTCacheEdit *edit)
}
/* calculate a tree for finding nearest emitter's vertice */
-static void recalc_emitter_field(Object *ob, ParticleSystem *psys)
+void recalc_emitter_field(Object *ob, ParticleSystem *psys)
{
DerivedMesh *dm=psys_get_modifier(ob, psys)->dm;
PTCacheEdit *edit= psys->edit;
@@ -1178,7 +1180,7 @@ static void PE_update_selection(Scene *scene, Object *ob, int useflag)
point->flag &= ~PEP_EDIT_RECALC;
}
-static void update_world_cos(Object *ob, PTCacheEdit *edit)
+void update_world_cos(Object *ob, PTCacheEdit *edit)
{
ParticleSystem *psys = edit->psys;
ParticleSystemModifierData *psmd= psys_get_modifier(ob, psys);
@@ -4452,7 +4454,7 @@ int PE_undo_valid(Scene *scene)
return 0;
}
-static void PTCacheUndo_clear(PTCacheEdit *edit)
+void PTCacheUndo_clear(PTCacheEdit *edit)
{
PTCacheUndo *undo;
@@ -4553,7 +4555,7 @@ int PE_minmax(Scene *scene, float min[3], float max[3])
/************************ particle edit toggle operator ************************/
/* initialize needed data for bake edit */
-static void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys)
+void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys)
{
PTCacheEdit *edit;
ParticleSystemModifierData *psmd = (psys) ? psys_get_modifier(ob, psys) : NULL;
diff --git a/source/blender/editors/physics/particle_object.c b/source/blender/editors/physics/particle_object.c
index 5a61f77e5a8..37ee7088db9 100644
--- a/source/blender/editors/physics/particle_object.c
+++ b/source/blender/editors/physics/particle_object.c
@@ -40,18 +40,22 @@
#include "BLI_math.h"
#include "BLI_listbase.h"
#include "BLI_utildefines.h"
+#include "BLI_string.h"
#include "BKE_context.h"
#include "BKE_depsgraph.h"
#include "BKE_DerivedMesh.h"
#include "BKE_cdderivedmesh.h"
+#include "BKE_effect.h"
#include "BKE_global.h"
+#include "BKE_library.h"
#include "BKE_main.h"
+#include "BKE_modifier.h"
+#include "BKE_object.h"
#include "BKE_particle.h"
#include "BKE_pointcache.h"
#include "BKE_report.h"
-
#include "RNA_access.h"
#include "RNA_define.h"
@@ -62,8 +66,31 @@
#include "ED_screen.h"
#include "ED_object.h"
+#include "UI_resources.h"
+
#include "physics_intern.h"
+extern void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys);
+extern void PTCacheUndo_clear(PTCacheEdit *edit);
+extern void recalc_lengths(PTCacheEdit *edit);
+extern void recalc_emitter_field(Object *ob, ParticleSystem *psys);
+extern void update_world_cos(Object *ob, PTCacheEdit *edit);
+
+#define KEY_K PTCacheEditKey *key; int k
+#define POINT_P PTCacheEditPoint *point; int p
+#define LOOP_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++)
+#define LOOP_VISIBLE_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (!(point->flag & PEP_HIDE))
+#define LOOP_SELECTED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point_is_selected(point))
+#define LOOP_UNSELECTED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (!point_is_selected(point))
+#define LOOP_EDITED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point->flag & PEP_EDIT_RECALC)
+#define LOOP_TAGGED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point->flag & PEP_TAG)
+#define LOOP_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++)
+#define LOOP_VISIBLE_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if (!(key->flag & PEK_HIDE))
+#define LOOP_SELECTED_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if ((key->flag & PEK_SELECT) && !(key->flag & PEK_HIDE))
+#define LOOP_TAGGED_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if (key->flag & PEK_TAG)
+
+#define KEY_WCO (key->flag & PEK_USE_WCO ? key->world_co : key->co)
+
/********************** particle system slot operators *********************/
static int particle_system_add_exec(bContext *C, wmOperator *UNUSED(op))
@@ -623,37 +650,32 @@ void PARTICLE_OT_disconnect_hair(wmOperatorType *ot)
RNA_def_boolean(ot->srna, "all", 0, "All hair", "Disconnect all hair systems from the emitter mesh");
}
-static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
+static bool remap_hair_emitter(Scene *scene, Object *UNUSED(ob), ParticleSystem *psys,
+ Object *target_ob, DerivedMesh *target_dm, ParticleSystem *target_psys, PTCacheEdit *target_edit,
+ bool from_world_space, bool to_world_space)
{
- ParticleSystemModifierData *psmd = psys_get_modifier(ob, psys);
- ParticleData *pa;
- PTCacheEdit *edit;
- PTCacheEditPoint *point;
- PTCacheEditKey *ekey = NULL;
- HairKey *key;
+ ParticleData *pa, *tpa;
+ PTCacheEditPoint *edit_point;
+ PTCacheEditKey *ekey;
BVHTreeFromMesh bvhtree= {NULL};
- BVHTreeNearest nearest;
MFace *mface = NULL, *mf;
MEdge *medge = NULL, *me;
MVert *mvert;
- DerivedMesh *dm = NULL;
+ DerivedMesh *dm;
int numverts;
int i, k;
- float hairmat[4][4], imat[4][4];
- float v[4][3], vec[3];
- if (!psys || !psys->part || psys->part->type != PART_HAIR || !psmd->dm)
+ if (!psys->part || psys->part->type != PART_HAIR || !target_psys->part || target_psys->part->type != PART_HAIR)
return false;
- edit= psys->edit;
- point= edit ? edit->points : NULL;
+ edit_point = target_edit ? target_edit->points : NULL;
- if (psmd->dm->deformedOnly) {
- /* we don't want to mess up psmd->dm when converting to global coordinates below */
- dm = psmd->dm;
+ if (target_dm->deformedOnly) {
+ /* we don't want to mess up target_dm when converting to global coordinates below */
+ dm = target_dm;
}
else {
- dm = mesh_get_derived_deform(scene, ob, CD_MASK_BAREMESH);
+ dm = mesh_get_derived_deform(scene, target_ob, CD_MASK_BAREMESH);
}
/* don't modify the original vertices */
dm = CDDM_copy(dm);
@@ -662,12 +684,11 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
DM_ensure_tessface(dm);
numverts = dm->getNumVerts(dm);
-
mvert = dm->getVertArray(dm);
/* convert to global coordinates */
for (i=0; i<numverts; i++)
- mul_m4_v3(ob->obmat, mvert[i].co);
+ mul_m4_v3(target_ob->obmat, mvert[i].co);
if (dm->getNumTessFaces(dm) != 0) {
mface = dm->getTessFaceArray(dm);
@@ -682,13 +703,17 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
return false;
}
- for (i=0, pa= psys->particles; i<psys->totpart; i++, pa++) {
- key = pa->hair;
+ for (i = 0, tpa = target_psys->particles, pa = psys->particles;
+ i < target_psys->totpart;
+ i++, tpa++, pa++) {
+
+ const float *co = from_world_space ? pa->hair[0].co : pa->hair[0].world_co;
+ BVHTreeNearest nearest;
nearest.index = -1;
nearest.dist_sq = FLT_MAX;
- BLI_bvhtree_find_nearest(bvhtree.tree, key->co, &nearest, bvhtree.nearest_callback, &bvhtree);
+ BLI_bvhtree_find_nearest(bvhtree.tree, co, &nearest, bvhtree.nearest_callback, &bvhtree);
if (nearest.index == -1) {
if (G.debug & G_DEBUG)
@@ -697,6 +722,8 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
}
if (mface) {
+ float v[4][3];
+
mf = &mface[nearest.index];
copy_v3_v3(v[0], mvert[mf->v1].co);
@@ -704,44 +731,71 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
copy_v3_v3(v[2], mvert[mf->v3].co);
if (mf->v4) {
copy_v3_v3(v[3], mvert[mf->v4].co);
- interp_weights_poly_v3(pa->fuv, v, 4, nearest.co);
+ interp_weights_poly_v3(tpa->fuv, v, 4, nearest.co);
}
else
- interp_weights_poly_v3(pa->fuv, v, 3, nearest.co);
+ interp_weights_poly_v3(tpa->fuv, v, 3, nearest.co);
+ tpa->foffset = 0.0f;
- pa->num = nearest.index;
- pa->num_dmcache = psys_particle_dm_face_lookup(ob, psmd->dm, pa->num, pa->fuv, NULL);
+ tpa->num = nearest.index;
+ tpa->num_dmcache = psys_particle_dm_face_lookup(target_ob, dm, tpa->num, tpa->fuv, NULL);
}
else {
me = &medge[nearest.index];
- pa->fuv[1] = line_point_factor_v3(nearest.co,
- mvert[me->v2].co,
- mvert[me->v2].co);
- pa->fuv[0] = 1.0f - pa->fuv[1];
- pa->fuv[2] = pa->fuv[3] = 0.0f;
+ tpa->fuv[1] = line_point_factor_v3(nearest.co,
+ mvert[me->v1].co,
+ mvert[me->v2].co);
+ tpa->fuv[0] = 1.0f - tpa->fuv[1];
+ tpa->fuv[2] = tpa->fuv[3] = 0.0f;
+ tpa->foffset = 0.0f;
- pa->num = nearest.index;
- pa->num_dmcache = -1;
+ tpa->num = nearest.index;
+ tpa->num_dmcache = -1;
}
- psys_mat_hair_to_global(ob, psmd->dm, psys->part->from, pa, hairmat);
- invert_m4_m4(imat, hairmat);
-
- sub_v3_v3v3(vec, nearest.co, key->co);
-
- if (point) {
- ekey = point->keys;
- point++;
- }
-
- for (k=0, key=pa->hair; k<pa->totkey; k++, key++) {
- add_v3_v3(key->co, vec);
- mul_m4_v3(imat, key->co);
-
- if (ekey) {
- ekey->flag |= PEK_USE_WCO;
- ekey++;
+ /* translate hair keys */
+ {
+ HairKey *key, *tkey;
+ float hairmat[4][4], imat[4][4];
+ float offset[3];
+
+ /* note: using target_dm here, which is in target_ob object space and has full modifiers */
+ psys_mat_hair_to_global(target_ob, target_dm, target_psys->part->from, tpa, hairmat);
+ invert_m4_m4(imat, hairmat);
+
+ /* offset in world space */
+ sub_v3_v3v3(offset, nearest.co, co);
+ {
+ SimDebugData *dd = psys->clmd ? psys->clmd->debug_data : NULL;
+ BKE_sim_debug_data_add_dot(dd, nearest.co, 1,1,1, "particle matrix", 689, i);
+
+ BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[0], 1,0,0, "particle matrix", 222, i);
+ BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[1], 0,1,0, "particle matrix", 333, i);
+ BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[2], 0,0,1, "particle matrix", 444, i);
+ }
+
+ if (edit_point) {
+ for (k=0, key=pa->hair, tkey=tpa->hair, ekey = edit_point->keys; k<tpa->totkey; k++, key++, tkey++, ekey++) {
+ const float *co_orig = from_world_space ? key->co : key->world_co;
+
+ add_v3_v3v3(tkey->co, co_orig, offset);
+ if (!to_world_space)
+ mul_m4_v3(imat, tkey->co);
+
+ ekey->flag |= PEK_USE_WCO;
+ }
+
+ edit_point++;
+ }
+ else {
+ for (k=0, key=pa->hair, tkey=tpa->hair; k<tpa->totkey; k++, key++, tkey++) {
+ const float *co_orig = from_world_space ? key->co : key->world_co;
+
+ add_v3_v3v3(tkey->co, co_orig, offset);
+ if (!to_world_space)
+ mul_m4_v3(imat, tkey->co);
+ }
}
}
}
@@ -749,15 +803,30 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
free_bvhtree_from_mesh(&bvhtree);
dm->release(dm);
- psys_free_path_cache(psys, psys->edit);
+ psys_free_path_cache(target_psys, target_edit);
- psys->flag &= ~PSYS_GLOBAL_HAIR;
-
- PE_update_object(scene, ob, 0);
+ PE_update_object(scene, target_ob, 0);
return true;
}
+static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
+{
+ ParticleSystemModifierData *psmd;
+ const bool from_global = psys->flag & PSYS_GLOBAL_HAIR;
+ const bool to_global = false;
+
+ if (!psys)
+ return false;
+
+ psmd = psys_get_modifier(ob, psys);
+ if (!psmd->dm)
+ return false;
+
+ psys->flag &= ~PSYS_GLOBAL_HAIR;
+ return remap_hair_emitter(scene, ob, psys, ob, psmd->dm, psys, psys->edit, from_global, to_global);
+}
+
static int connect_hair_exec(bContext *C, wmOperator *op)
{
Scene *scene= CTX_data_scene(C);
@@ -805,3 +874,188 @@ void PARTICLE_OT_connect_hair(wmOperatorType *ot)
RNA_def_boolean(ot->srna, "all", 0, "All hair", "Connect all hair systems to the emitter mesh");
}
+/************************ particle system copy operator *********************/
+
+static bool find_object_pair(bContext *C, ReportList *reports, Object **r_ob_to, Object **r_ob_from)
+{
+ ListBase selected;
+ CollectionPointerLink *link;
+ Object *ob_from, *ob_to;
+
+ ob_to = CTX_data_active_object(C);
+ if (!ob_to) {
+ BKE_report(reports, RPT_ERROR, "No active object");
+ return false;
+ }
+
+ ob_from = NULL;
+ CTX_data_selected_objects(C, &selected);
+ for (link = selected.first; link; link = link->next) {
+ Object *ob = link->ptr.data;
+ if (ob != ob_to) {
+ if (ob_from)
+ break; /* indicates multiple selected objects */
+ else
+ ob_from = ob;
+ }
+ }
+ BLI_freelistN(&selected);
+
+ if (!ob_from) {
+ BKE_report(reports, RPT_ERROR, "No non-active selected object");
+ return false;
+ }
+ else if (link) {
+ BKE_report(reports, RPT_ERROR, "Multiple non-active selected objects");
+ return false;
+ }
+
+ *r_ob_to = ob_to;
+ *r_ob_from = ob_from;
+ return true;
+}
+
+static void copy_particle_edit(Scene *scene, Object *ob, ParticleSystem *psys, ParticleSystem *psys_from)
+{
+ PTCacheEdit *edit_from = psys_from->edit, *edit;
+ ParticleData *pa;
+ KEY_K;
+ POINT_P;
+
+ if (!edit_from)
+ return;
+
+ edit = MEM_dupallocN(edit_from);
+ edit->psys = psys;
+ psys->edit = edit;
+
+ edit->pathcache = NULL;
+ BLI_listbase_clear(&edit->pathcachebufs);
+
+ edit->emitter_field = NULL;
+ edit->emitter_cosnos = NULL;
+
+ BLI_listbase_clear(&edit->undo);
+ edit->curundo = NULL;
+
+ edit->points = MEM_dupallocN(edit_from->points);
+ pa = psys->particles;
+ LOOP_POINTS {
+ HairKey *hkey = pa->hair;
+
+ point->keys= MEM_dupallocN(point->keys);
+ LOOP_KEYS {
+ key->co = hkey->co;
+ key->time = &hkey->time;
+ key->flag = hkey->editflag;
+ if (!(psys->flag & PSYS_GLOBAL_HAIR)) {
+ key->flag |= PEK_USE_WCO;
+ hkey->editflag |= PEK_USE_WCO;
+ }
+
+ hkey++;
+ }
+
+ pa++;
+ }
+ update_world_cos(ob, edit);
+
+ UI_GetThemeColor3ubv(TH_EDGE_SELECT, edit->sel_col);
+ UI_GetThemeColor3ubv(TH_WIRE, edit->nosel_col);
+
+ recalc_lengths(edit);
+ recalc_emitter_field(ob, psys);
+ PE_update_object(scene, ob, true);
+
+ PTCacheUndo_clear(edit);
+ PE_undo_push(scene, "Original");
+}
+
+static int copy_particle_systems_exec(bContext *C, wmOperator *op)
+{
+ Scene *scene = CTX_data_scene(C);
+ Object *ob_from, *ob_to;
+ ParticleSystem *psys, *psys_from;
+ ModifierData *md, *md_next;
+ DerivedMesh *final_dm;
+ int i;
+
+ if (!find_object_pair(C, op->reports, &ob_to, &ob_from))
+ return OPERATOR_CANCELLED;
+
+ if (BLI_listbase_is_empty(&ob_from->particlesystem)) {
+ BKE_reportf(op->reports, RPT_ERROR, "Object %200s has no particle systems", ob_from->id.name+2);
+ return OPERATOR_CANCELLED;
+ }
+
+ /* XXX in theory it could be nice to not delete existing particle systems,
+ * but the current code for copying assumes that the target object list is empty ...
+ */
+ for (md = ob_to->modifiers.first; md; md = md_next) {
+ md_next = md->next;
+
+ /* remove all particle system modifiers as well,
+ * these need to sync to the particle system list
+ */
+ if (ELEM(md->type, eModifierType_ParticleSystem, eModifierType_DynamicPaint, eModifierType_Smoke)) {
+ BLI_remlink(&ob_to->modifiers, md);
+ modifier_free(md);
+ }
+ }
+ BKE_object_free_particlesystems(ob_to);
+
+ /* For remapping we need a valid DM.
+ * Because the modifier is appended at the end it's safe to use
+ * the final DM of the object without particles
+ */
+ if (ob_to->derivedFinal)
+ final_dm = ob_to->derivedFinal;
+ else
+ final_dm = mesh_get_derived_final(scene, ob_to, CD_MASK_BAREMESH);
+
+ BKE_object_copy_particlesystems(ob_to, ob_from);
+ for (psys = ob_to->particlesystem.first, psys_from = ob_from->particlesystem.first, i = 0;
+ psys;
+ psys = psys->next, psys_from = psys_from->next, ++i) {
+
+ ParticleSystemModifierData *psmd;
+
+ /* add a particle system modifier for each system */
+ md = modifier_new(eModifierType_ParticleSystem);
+ psmd = (ParticleSystemModifierData *)md;
+ /* push on top of the stack, no use trying to reproduce old stack order */
+ BLI_addtail(&ob_to->modifiers, md);
+
+ BLI_snprintf(md->name, sizeof(md->name), "ParticleSystem %i", i);
+ modifier_unique_name(&ob_to->modifiers, (ModifierData *)psmd);
+
+ psmd->psys = psys;
+ psmd->dm = CDDM_copy(final_dm);
+ CDDM_calc_normals(psmd->dm);
+
+ if (psys_from->edit)
+ copy_particle_edit(scene, ob_to, psys, psys_from);
+
+ remap_hair_emitter(scene, ob_from, psys_from, ob_to, psmd->dm, psys, psys->edit, false, false);
+
+ /* tag for recalc */
+ psys->recalc |= PSYS_RECALC_RESET;
+ }
+
+ DAG_id_tag_update(&ob_to->id, OB_RECALC_DATA);
+ WM_main_add_notifier(NC_OBJECT | ND_PARTICLE | NA_EDITED, ob_to);
+
+ return OPERATOR_FINISHED;
+}
+
+void PARTICLE_OT_copy_particle_systems(wmOperatorType *ot)
+{
+ ot->name = "Copy Particle Systems";
+ ot->description = "Copy particle systems to the active object";
+ ot->idname = "PARTICLE_OT_copy_particle_systems";
+
+ ot->exec = copy_particle_systems_exec;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+}
diff --git a/source/blender/editors/physics/physics_intern.h b/source/blender/editors/physics/physics_intern.h
index 705235a376e..666ed2397d2 100644
--- a/source/blender/editors/physics/physics_intern.h
+++ b/source/blender/editors/physics/physics_intern.h
@@ -72,6 +72,7 @@ void PARTICLE_OT_target_move_up(struct wmOperatorType *ot);
void PARTICLE_OT_target_move_down(struct wmOperatorType *ot);
void PARTICLE_OT_connect_hair(struct wmOperatorType *ot);
void PARTICLE_OT_disconnect_hair(struct wmOperatorType *ot);
+void PARTICLE_OT_copy_particle_systems(struct wmOperatorType *ot);
void PARTICLE_OT_dupliob_copy(struct wmOperatorType *ot);
void PARTICLE_OT_dupliob_remove(struct wmOperatorType *ot);
diff --git a/source/blender/editors/physics/physics_ops.c b/source/blender/editors/physics/physics_ops.c
index 7f516ea2b2f..c765bff796e 100644
--- a/source/blender/editors/physics/physics_ops.c
+++ b/source/blender/editors/physics/physics_ops.c
@@ -80,6 +80,7 @@ static void operatortypes_particle(void)
WM_operatortype_append(PARTICLE_OT_target_move_down);
WM_operatortype_append(PARTICLE_OT_connect_hair);
WM_operatortype_append(PARTICLE_OT_disconnect_hair);
+ WM_operatortype_append(PARTICLE_OT_copy_particle_systems);
WM_operatortype_append(PARTICLE_OT_dupliob_copy);
WM_operatortype_append(PARTICLE_OT_dupliob_remove);