Manipulating particles in python

Here is the script to get particles location

import bpy

# Dependancy graph
degp = bpy.context.evaluated_depsgraph_get()

# Emitter Object
object = bpy.data.objects["Cube"]

# Evaluate the depsgraph (Important step)
particle_systems = object.evaluated_get(degp).particle_systems

# All particles of first particle-system which has index "0"
particles = particle_systems[0].particles

# Total Particles
totalParticles = len(particles)

# length of 1D array or list = 3*totalParticles, "3" due to XYZ in vector/location.
# If the length is wrong then it will give you an error "internal error setting the array"
flatList = [0]*(3*totalParticles)

# To get the loaction of all particles
particles.foreach_get("location", flatList)

With “foreach_set(“location”, flatList)” function, you can set the location of the particles.

5 Likes

Thanks, this worked great!

Hi and thanks for the code snippet!

It does seem to work great for getting the particle locations. However, in my current situation I need to be able to set particle locations and that does not work with foreach_set("location", flatList) (using most recent Blender 2.80 build on Windows).

Here’s an example (using initial scene with a particle system of 3 particles on the Cube-object):

import bpy

# --- PASS 1 ---
degp = bpy.context.evaluated_depsgraph_get()
object = bpy.data.objects["Cube"]
particle_systems = object.evaluated_get(degp).particle_systems
particles = particle_systems[0].particles
totalParticles = len(particles)
flatList = [0]*(3*totalParticles)

# additionally set the location of all particle locations to [0, 0, 0]
particles.foreach_set("location", flatList)

# ... and confirm they are set as intended
particles.foreach_get("location", flatList)
print('PASS 1: {}'.format(flatList))

# --- Toggle back and forth to update viewport ---
bpy.ops.particle.particle_edit_toggle()
bpy.ops.particle.particle_edit_toggle()

# --- PASS 2 ---
degp = bpy.context.evaluated_depsgraph_get()
object = bpy.data.objects["Cube"]
particle_systems = object.evaluated_get(degp).particle_systems
particles = particle_systems[0].particles
totalParticles = len(particles)
flatList = [0]*(3*totalParticles)
particles.foreach_get("location", flatList)
print('PASS 2: {}'.format(flatList))

Expected output: all particles remain at [0, 0, 0]. Actual output:

PASS 1: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
PASS 2: [-0.84..., -1.03..., 0.75..., -1.0, -0.32..., 0.12..., -0.62..., 0.99..., 0.63...]

I don’t know anything about the depsgraph and such, but I’d very much like to find a way to set particle locations by script. Is there another way or a workaround?

2 Likes

You only need this part of the script to set the location of the particles

import bpy

# --- PASS 1 ---
degp = bpy.context.evaluated_depsgraph_get()
object = bpy.data.objects["Cube"]
particle_systems = object.evaluated_get(degp).particle_systems
particles = particle_systems[0].particles
totalParticles = len(particles)
flatList = [0]*(3*totalParticles)

# additionally set the location of all particle locations to [0, 0, 0]
particles.foreach_set("location", flatList)

# ... and confirm they are set as intended
particles.foreach_get("location", flatList)
print('PASS 1: {}'.format(flatList))

This line bpy.ops.particle.particle_edit_toggle() then it refresh the cache or reset the location of particles. That is why you are getting different output in the PASS-2.

1 Like

Thanks 3DSinghVFX,

Ok. With my example I wanted to show that the written data was not persistent, but you are saying this is actually the expected behavior since particle_edit_toggle() resets the cache?

I included it here because in previous version (2.79) it updated the viewport (although it still rendered fine without it). I’m not saying it was the correct method, but it did the job.

The problem is that even without it, after only executing “PASS 1” as you advised (and is actually what I tried first), the code may print the set value as expected, but it is not reflected in the viewport, nor the render. It really doesn’t have any effect.

So, what am I missing? Something else cache-related? Is it a bug? Does it work for everyone else? I hope it’s just some simple thing I’ve missed and is easily fixed, but I’ve been trying for a while now…

Here I made an example which updates the location of particles on each frame,

import bpy
import numpy as np

degp = bpy.context.evaluated_depsgraph_get()
object = bpy.data.objects["Cube"]

def particleSetter(self):
    particle_systems = object.evaluated_get(degp).particle_systems
    particles = particle_systems[0].particles
    totalParticles = len(particles)

    scene = bpy.context.scene
    cFrame = scene.frame_current
    sFrame = scene.frame_start
    
    #at start-frame, clear the particle cache
    if cFrame == sFrame:
        psSeed = object.particle_systems[0].seed 
        object.particle_systems[0].seed  = psSeed
    
    # sin function as location of particles            
    data = 5.0*np.sin(cFrame/20.0)
    flatList = [data]*(3*totalParticles)

    # additionally set the location of all particle locations to flatList
    particles.foreach_set("location", flatList)

#clear the post frame handler
bpy.app.handlers.frame_change_post.clear() 

#run the function on each frame
bpy.app.handlers.frame_change_post.append(particleSetter)  
2 Likes

Awesome!

For reference, I modified your code a little so that it also initiates the particle system and can be run directly from startup:

import bpy
import numpy as np

def particleSetter(self):
    particle_systems = object.evaluated_get(degp).particle_systems
    particles = particle_systems[0].particles
    totalParticles = len(particles)

    scene = bpy.context.scene
    cFrame = scene.frame_current
    sFrame = scene.frame_start

    # at start-frame, clear the particle cache
    if cFrame == sFrame:
        psSeed = object.particle_systems[0].seed
        object.particle_systems[0].seed = psSeed

    # Rotate particles based on index (t_p) and frame (t_f)
    t_p = np.linspace(0, 2*np.pi, totalParticles, endpoint=False)
    t_f = cFrame / 20.0
    data = np.array([np.sin(t_p + t_f), np.cos(t_p + t_f), np.zeros(t_p.shape)]).T
    flatList = data.ravel()

    # Set the location of all particle locations to flatList
    particles.foreach_set("location", flatList)
    

# Prepare particle system
object = bpy.data.objects["Cube"]
object.modifiers.new("ParticleSystem", 'PARTICLE_SYSTEM')
object.particle_systems[0].settings.count = 10
object.particle_systems[0].settings.frame_start = 1
object.particle_systems[0].settings.frame_end = 1
object.particle_systems[0].settings.lifetime = 1000
object.show_instancer_for_viewport = False
degp = bpy.context.evaluated_depsgraph_get()

#clear the post frame handler
bpy.app.handlers.frame_change_post.clear()

#run the function on each frame
bpy.app.handlers.frame_change_post.append(particleSetter)

# Update to a frame where particles are updated
bpy.context.scene.frame_current = 2

screenshot

Now I’ll take some time to digest this and see if I can make it work for all my needs. (I also want to do something similar with hair, needing to set particle.hair_keys[].co, but should I need help with that I should refrain from hijacking this thread).

Anyway: It works great! I’d never have come up with this solution myself. Thanks alot 3DSinghVFX!

2 Likes

Just to add, I have added the Particle Output Node to Animation Nodes which allows to control location, velocity, rotation, and die-time of particles directly with Animation Nodes for Blender 2.79/2.80 :slight_smile:

4 Likes

Hello,
I would need to modify particle sizes. Your “particle output node” has no socket for this.
Is it impossible ?

I tried to modify your python script and… it doesn’t work :frowning:

Thanks anyway for your great work

1 Like

Hi,
The current particle-system of Blender does not allow to change the size of particles dynamically. However, you can use a texture to control the size of particles but that is very limited (in 3D). On the other hand, if you are using a small number of particles then use your own objects for particles with the help of Object Instancer or Mesh Transform node, with that you can change the size of the particles :slight_smile:

1 Like

Hi there, and thanks for the good help regarding editing particle systems in 2.8x via python. I have a related question after trying out your code samples : how to access particles position at previous frames ? What I am trying to do cannot be written as a function of time only (or particle index, …), and need particles position at the previous frame. When playing with particles.foreach_get, it seems like this method only returns their position at the first frame of the system, and not the position I’ve updated via python (for the test for the first 10 frames of my scene). Thanks in advance !

You can access the previous frame particle-position with Animation Nodes, on blender stack exchange, Omar has given a very nice example: https://blender.stackexchange.com/a/105178/62606. Let me know if you need further help :slight_smile:

@3DSinghVFX

I am trying to use the code you made for particleSetter, based on @mxrten 's version. Basically I have made an SPH simulation in a external program, where I now import all the points into blender to vizualise. I have gotten it to work as seen;

But my problem is now that the playback speed is incredibly slow ie. I only get two frames per second and this is only 450k particles…

Importing the data is not the problem since I can preallocate data so any suggestions of how to make the particleSetter algorithm faster or maybe another way to import particles more efficiently?

Kind regards

I’m trying to work with particle.location, for a Hair particle system. When accessing the particle locations through the Python API…

        for p in particles:
            loc = Matrix.Translation(p.location)

… I’m noticing that the particle locations don’t seem to match what I’m seeing on screen. In this example, where I’d expect every particle’s location to be within +/- 3 from the origin, I get p.location[0]=6.400... and p.prev_location[0]=5.600..., despite the fact that this model isn’t animated, and playing the timeline doesn’t move anything.

Any idea why that would be? I’ve disabled Hair Dynamics and Field Weights, but no luck. I’d just like to access the original locations as shown on screen. This is Blender 2.81 if that’s relevant.

Hi @donmccurdy,
from the screenshot it seems that you set some custom scale to your emitter(Cube object). Try to apply scale before running the script.

Amazing topic !!!

i’m the dev of scatter, anyone here know if its possible to batch delete X particles depending on for example altitude of a terrain or slope ?

this could be so handy for creating worlds

, see below

I think I had done that already, as long as the scale is 1,1,1 in the Object panel that’s it, right? Screenshot:

My example .blend is attached, and a simplified version of my script:

depg = bpy.context.evaluated_depsgraph_get()
m = bpy.context.selected_objects[0]
ps = m.evaluated_get(depg).particle_systems[0]
p = ps.particles[0]

p.location
# -> Vector((-6.40000057220459, -0.5430816411972046, -0.2352956384420395))

p.prev_location
# -> Vector((-5.600000381469727, -0.5430816411972046, -0.2352956384420395))

^As will (hopefully) be visible in the file, I don’t expect any of these particles to move, so the difference between location and prev_location alone is confusing, in addition to the positions not being what I think I’m seeing on screen.

@BD3D This blog post, and specifically section four on Weight Painting, might be what you’re looking for:

1 Like

I was searching’ for an alternative, in forest pack they can do this instantly, with blender weight painting it’s much more computer demanding to generate the slope weightmap. Not interactive :frowning:

What about a gradient texture in Z?