Build a Blender particle system on a mesh: add a system, choose EMITTER or HAIR settings, set count, emission frame range, lifetime, initial velocity, physics and effector weights such as gravity, choose a real render path (HALO, OBJECT, COLLECTION, PATH, LINE, or NONE) and a separate viewport display, add children only when needed, and verify the result through the evaluated depsgraph and a scripted audit. Use when a mesh needs emitted particles or hair strands and the settings need to be set, checked, or handed off with evidence.
./install.sh blender-particle-systems-basicsname: blender_particle_systems_basics description: "Build a Blender particle system on a mesh: add a system, choose EMITTER or HAIR settings, set count, emission frame range, lifetime, initial velocity, physics and effector weights such as gravity, choose a real render path (HALO, OBJECT, COLLECTION, PATH, LINE, or NONE) and a separate viewport display, add children only when needed, and verify the result through the evaluated depsgraph and a scripted audit. Use when a mesh needs emitted particles or hair strands and the settings need to be set, checked, or handed off with evidence." metadata: { "includeInPrompt": true }
Put a working particle system on a mesh and prove that the settings are what the record says they are. The work runs in a fixed order: start from a clean mesh, add one system and name it, decide emitter or hair before touching any other setting, set emission and lifetime, set velocity and physics for emitters or length and steps for hair, choose the render path and the viewport display as separate decisions, and check the evaluated system at a stated frame before saving. Particle settings are data on the system. Guessing at them from a viewport view is how counts, lifetimes, and gravity weights drift away from the file.
Scope:
~/.local/bin/blender --version returned on the drafting machine, started as ~/.local/bin/blender -b --factory-startup --python script.py. Operators called and returned FINISHED: bpy.ops.mesh.primitive_cube_add, bpy.ops.mesh.primitive_uv_sphere_add, bpy.ops.mesh.primitive_ico_sphere_add, bpy.ops.object.particle_system_add, bpy.ops.object.particle_system_remove, bpy.ops.particle.duplicate_particle_system, bpy.ops.particle.particle_edit_toggle, bpy.ops.object.mode_set with PARTICLE_EDIT and OBJECT, bpy.ops.wm.read_factory_settings, bpy.ops.wm.save_as_mainfile, and bpy.ops.wm.open_mainfile. A cube at size 2.0 measured 8 vertices and 6 polygons, an icosphere at subdivisions 2 measured 42 vertices and 80 polygons, and a UV sphere at segments 16 and ring_count 8 measured 114 vertices and 128 polygons. Properties read or set with the values stated in the Workflow: on ParticleSettings, type, count, frame_start, frame_end, lifetime, lifetime_random, emit_from, distribution, physics_type, integrator, normal_factor, object_factor, factor_random, tangent_factor, tangent_phase, object_align_factor, mass, particle_size, size_random, damping, drag_factor, brownian_factor, timestep, subframes, use_rotations, rotation_mode, phase_factor, render_type, instance_object, instance_collection, use_whole_collection, display_method, display_size, display_percentage, line_length_head, line_length_tail, trail_count, path_start, path_end, hair_length, hair_step, display_step, render_step, use_hair_bspline, use_strand_primitive, child_type, child_percent, rendered_child_count, and child_size; on ParticleSystem, name, seed, settings, particles, child_particles, and use_hair_dynamics; and on the settings effector_weights collection, gravity, all, force, and wind, with the full weight list read as gravity, all, force, vortex, magnetic, wind, curve_guide, texture, harmonic, charge, lennardjones, boid, turbulence, drag, and smokeflow, each 1.0 in a factory scene. Enums read in full: type EMITTER and HAIR; emit_from VERT, FACE, and VOLUME; distribution JIT, RAND, and GRID; physics_type NO, NEWTON, KEYED, BOIDS, and FLUID; render_type NONE, HALO, LINE, PATH, OBJECT, and COLLECTION for an emitter. Evaluated checks: an emitter at count 200 held 200 particles in the evaluated depsgraph at frames 1 and 10 while the unevaluated system held 0; an emitter at count 10, frame_start 5.0, frame_end 15.0, and lifetime 30.0 gave its first three evaluated particles at frame 20 birth times 5.0, 6.0, and 7.0, die times 35.0, 36.0, and 37.0, and lifetime 30.0 each; a hair system at count 100 evaluated to 100 particles and its first particle carried 6 hair_keys at hair_step 5; and setting child_type to SIMPLE on a 100 count system gave 1000 evaluated child_particles for both an emitter and a hair system. The audit fixture run in the Operating Rules section returned objects_total: 3, mesh_objects: 3, vertices_total: 164, polygons_total: 214, particle_systems_total: 2, emitter_systems: 1, hair_systems: 1, and verdict: PASS, on two separate audit runs with identical output. Probes that failed, with the fix applied in this skill: setting render_type to HALO or LINE on a HAIR system failed, because the accepted values in that state are NONE, PATH, OBJECT, and COLLECTION, so hair is never assigned HALO or LINE here; setting display_method to DOT, CIRC, CROSS, or AXIS on a HAIR system failed, because the accepted values in that state are NONE, RENDER, and PATH, so hair display is left at RENDER here; bpy.ops.object.particle_edit_toggle could not be found, so particle edit mode is entered with bpy.ops.particle.particle_edit_toggle instead; bpy.ops.object.particle_system_add failed its poll check with no active object, so a mesh is made active first; and assigning type FUR failed, because EMITTER and HAIR are the only type values. Names that do not exist in 5.1.1 and are not used here: ParticleSettings.gravity (gravity is effector_weights.gravity on the settings), ParticleSettings.initial_velocity, ParticleSettings.velocity_factor, and ParticleSettings.render_as. What was NOT verified: no render was run, so no render path is claimed to look correct on screen, and Cycles was not used; viewport playback smoothness, particle motion feel, and collision behavior on screen were not tested; no hair grooming stroke was run, so combing, cutting, and styling results in particle edit mode are untested and only entering and leaving that mode was checked; BOIDS, FLUID, and KEYED physics were set as enum values only, with no behavior check; and GUI panel layout was not exercised headless, so panel locations in a live session may differ from the data names used here.bpy.ops.mesh.primitive_cube_add(size=2.0) gives 8 vertices and 6 polygons, bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.25) gives 42 vertices and 80 polygons, and bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=1.0) gives 114 vertices and 128 polygons.bpy.ops.wm.save_as_mainfile. Adding a system is easy to undo in a session and harder to strip cleanly from a saved file that also holds later work.bpy.ops.object.particle_system_add(). The call failed its poll check with no active object in the probe run, so active object state comes first.particle_systems on the object, it creates a ParticleSettings data block, and it adds a modifier of type PARTICLE_SYSTEM to the object. Read all three back before changing anything.seed on the system when the layout needs to be repeatable. The audit prints system name and seed from the file.bpy.ops.object.particle_system_add() a second time gave systems named ParticleSystem and ParticleSystem.001 in the probe run. Remove a system with bpy.ops.object.particle_system_remove() and duplicate one with bpy.ops.particle.duplicate_particle_system(), both of which returned FINISHED in the probe runs.type on the ParticleSettings to EMITTER or HAIR. Those are the only two values in 5.1.1. An emitter makes free particles that are born, move, and die inside a frame range. A hair system makes strands rooted on the surface, and its count means roots rather than moving particles.bpy.data.particles.new(name=...) and assigned to a system through its settings property, which the probe run did with a new block that arrived as type EMITTER at count 1000. Prefer the add operator for normal work, because it creates the system, the settings, and the modifier together.count first. Count is the total particle budget for the system. Start small while tuning, for example 200, and raise it only after the motion and render path read correctly, because every later check costs more at a high count.frame_start and frame_end. Particles are born across that window. Probe evidence: at count 10, frame_start 5.0, and frame_end 15.0, the first three evaluated particles were born at frames 5.0, 6.0, and 7.0.lifetime in frames and lifetime_random as a 0.0 to 1.0 share of variation. Probe evidence: at lifetime 30.0 and lifetime_random 0.0, each checked particle had die_time equal to birth_time plus 30.0. A system whose frame_end plus lifetime runs past the scene end will still hold live particles when the scene stops, which may or may not be the intent. State the intent in the record.emit_from to VERT, FACE, or VOLUME, and distribution to JIT, RAND, or GRID. FACE with JIT is the factory pairing and suits a surface emitter. VOLUME fills the inside of a closed mesh. GRID places particles on a grid pattern and pairs with VOLUME work.normal_factor pushes particles along the surface normal, tangent_factor and tangent_phase push along the surface tangent, object_factor pushes along object motion, and object_align_factor is a vector that aligns velocity to chosen object axes.factor_random, a 0.0 to 1.0 share. Random spread at zero gives a clean, even stream that shows the base direction clearly. Raise it after the base direction is right, or direction errors and spread errors become hard to tell apart.physics_type at NEWTON for standard moving particles. The full enum is NO, NEWTON, KEYED, BOIDS, and FLUID. NO gives particles with no physics step. KEYED, BOIDS, and FLUID were set as enum values in the probe runs only, and this skill makes no behavior claim for them.mass (1.0 in a factory scene), particle_size and size_random, damping, drag_factor, and brownian_factor for random jitter. Each was read and set in the probe runs. Raise brownian_factor only when jitter is wanted, because it reads as noise in every other case.integrator takes EULER, VERLET, MIDPOINT, and RK4, with MIDPOINT as the factory value read in the probe run, plus timestep (0.04 in a factory scene, read as 0.03999999910593033) and subframes. Leave these at factory values until a fast particle tunnels or a curve bends wrong, then change timestep or subframes and record the change.effector_weights.gravity on the settings. There is no gravity property directly on ParticleSettings. Factory gravity weight is 1.0. The audit fixture uses 0.5 for a slower fall. Setting gravity weight to 0.0 removes gravity pull for that system while leaving other systems alone.all weight scales the group. Use the single kind weight when one effect should weaken and the rest should stay, for example effector_weights.wind at 0.7 while gravity stays at 1.0.use_rotations when the render path shows an object whose facing matters, then set rotation_mode (VEL is one value, aligning rotation to velocity) and phase_factor. Rotation settings on a HALO system have no visible object to turn, so pair this step with the render choice in step 7.render_type on the settings decides what a render draws for an emitter. The full emitter enum is NONE, HALO, LINE, PATH, OBJECT, and COLLECTION. NONE draws nothing in a render while the system still simulates, which suits a system kept as source data for another step.instance_object at each particle. Set render_type to OBJECT first, then assign a real mesh object to instance_object, as the audit fixture does with a small icosphere. An OBJECT system with no instance_object assigned has nothing to draw, so the audit prints instance_object for every system.instance_collection. The use_whole_collection flag, off in a factory scene, decides whether the whole collection appears at each particle or its objects are spread across particles. State which behavior the record intends.line_length_head and line_length_tail (0.0 and 0.5 in a factory scene) and trail_count. PATH draws a path per particle using path_start and path_end (0.0 and 1.0 in a factory scene). Both suit motion trails and streak effects.display_method decides what the viewport draws, and it is a separate setting from render_type. For an emitter it takes NONE, RENDER, DOT, CIRC, CROSS, and AXIS. RENDER shows the render path in the viewport. DOT, CIRC, CROSS, and AXIS show cheap markers that keep a dense system readable while tuning. display_size (0.1 in a factory scene) sizes those markers, and display_percentage (100 in a factory scene) shares how many particles the viewport shows.count as the number of roots on the surface. The audit fixture uses 500 roots on a UV sphere. Root placement follows the same emit_from values as emitters: VERT, FACE, or VOLUME, with FACE as the usual surface choice.hair_length for strand length (4.0 in a factory scene, 1.5 in the audit fixture) and hair_step for the number of steps along each strand. Probe evidence for what a step means in data: at hair_step 5, the first evaluated hair particle carried 6 hair_keys, one key at the root and one at each step end.display_step 2 and render_step 3 in a factory scene, plus flags use_hair_bspline and use_strand_primitive, both off in a factory scene. Raise render_step for smoother strand curves in the render stage and record the value, because display_step and render_step can differ and a viewport check at display_step does not prove the render state.use_hair_dynamics on the ParticleSystem, off by default. Turning it on in the probe run created the cloth data the system uses for dynamics, read back as a non-empty cloth property on the system. This skill sets the switch and records it. Dynamics behavior on screen was not verified.bpy.ops.particle.particle_edit_toggle(), which returned FINISHED in the probe run and left the object in PARTICLE_EDIT mode, as did bpy.ops.object.mode_set(mode='PARTICLE_EDIT'). Leave with mode OBJECT the same way.bpy.ops.object.particle_edit_toggle fail, so use the particle level call in any scripted step.is_edited on the system in the record when a file has passed through particle edit mode. It is a property on the ParticleSystem, read as False on an unedited system in the probe runs, and it tells the next stage whether hand edits exist in the file at all.child_type takes NONE, SIMPLE, and INTERPOLATED, with NONE as the factory value. SIMPLE places children around each parent. INTERPOLATED spreads children across the surface between parents.child_percent 10, rendered_child_count 100, child_size 1.0, and child_size_random 0.0 in a factory scene. The audit script in this skill does not print child settings, so a deliverable that uses children records these values in its settings record by hand, from the settings data.particles on an unevaluated ParticleSystem held 0 entries in the probe run for a system that held 200 in the evaluated depsgraph. Build the evaluated view in code by getting the evaluated object from bpy.context.evaluated_depsgraph_get() and reading its particle system, and set the frame first with bpy.context.scene.frame_set(frame).bpy.ops.wm.save_as_mainfile with a new file name each time. A particle file with the wrong gravity weight or seed is recovered from the previous version, not by retuning from memory.Deliver these items, in this order:
Run: ~/.local/bin/blender -b --factory-startup --python particle_audit.py -- --make-fixture particle_fixture.blend builds the worked fixture blend file. Then run: ~/.local/bin/blender -b --factory-startup --python particle_audit.py -- particle_fixture.blend to reproduce the output below. Run it on a real particle file with ~/.local/bin/blender -b --factory-startup --python particle_audit.py -- FILE.blend. Blender prints its own banner and a read line before the script output. The block below is the script output only.
Expected output for the fixture run:
text
fixture_file: particle_fixture.blend
objects_total: 3
mesh_objects: 3
OBJECT EmitterCube verts=8 polys=6 particle_systems=1
OBJECT HairHead verts=114 polys=128 particle_systems=1
OBJECT ParticleInstance verts=42 polys=80 particle_systems=0
PARTICLE object=EmitterCube system=EmitterMain type=EMITTER count=200 seed=7 render_type=OBJECT instance_object=ParticleInstance
EMITTER object=EmitterCube frame_start=1.0 frame_end=80.0 lifetime=40.0 lifetime_random=0.25 emit_from=FACE distribution=JIT physics_type=NEWTON normal_factor=2.0 factor_random=0.3 particle_size=0.08 gravity=0.5
PARTICLE object=HairHead system=HairMain type=HAIR count=500 seed=3 render_type=PATH instance_object=-
HAIR object=HairHead hair_length=1.5 hair_step=5 emit_from=FACE child_type=NONE
vertices_total: 164
polygons_total: 214
particle_systems_total: 2
emitter_systems: 1
hair_systems: 1
verdict: PASS
The fixture is built to show each audit result once: three mesh objects, one emitter system rendering a small icosphere through the OBJECT path at gravity weight 0.5, and one hair system at 500 roots with hair_length 1.5 and hair_step 5 rendering through PATH, which gives vertices_total: 164, polygons_total: 214, and a verdict of PASS.
particle_audit.py (runs inside Blender, uses bpy). Save this code as particle_audit.py:
python
#!/usr/bin/env python3
"""Audit particle system state in a Blender scene.
Runs inside Blender, not in plain Python:
blender -b --factory-startup --python particle_audit.py -- --make-fixture particle_fixture.blend
blender -b --factory-startup --python particle_audit.py -- particle_fixture.blend
The --make-fixture form builds a fixture with three mesh objects:
ParticleInstance (icosphere, the emitter render instance),
EmitterCube (cube, one EMITTER system), and HairHead (UV sphere,
one HAIR system), and prints two fixture lines. The audit form
opens the named blend, prints one line per mesh object sorted by
name, one block per particle system, summary counts, and a
verdict. PASS means exactly the fixture shape: three mesh
objects, two systems, one emitter and one hair. Any other shape
prints REVIEW. Blender banner lines are not script output.
"""
import sys
import bpy
def build_fixture(path):
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.25)
inst = bpy.context.active_object
inst.name = "ParticleInstance"
bpy.ops.mesh.primitive_cube_add(size=2.0)
emitter = bpy.context.active_object
emitter.name = "EmitterCube"
bpy.ops.object.particle_system_add()
eps = emitter.particle_systems[0]
eps.name = "EmitterMain"
eps.seed = 7
es = eps.settings
es.type = 'EMITTER'
es.count = 200
es.frame_start = 1.0
es.frame_end = 80.0
es.lifetime = 40.0
es.lifetime_random = 0.25
es.emit_from = 'FACE'
es.distribution = 'JIT'
es.physics_type = 'NEWTON'
es.normal_factor = 2.0
es.factor_random = 0.3
es.particle_size = 0.08
es.render_type = 'OBJECT'
es.instance_object = inst
es.effector_weights.gravity = 0.5
bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=1.0)
hair_obj = bpy.context.active_object
hair_obj.name = "HairHead"
bpy.ops.object.particle_system_add()
hps = hair_obj.particle_systems[0]
hps.name = "HairMain"
hps.seed = 3
hs = hps.settings
hs.type = 'HAIR'
hs.count = 500
hs.hair_length = 1.5
hs.hair_step = 5
hs.emit_from = 'FACE'
hs.render_type = 'PATH'
bpy.ops.wm.save_as_mainfile(filepath=path)
print(f"fixture_file: {path}")
print("fixture_objects: 3")
return 0
def audit(path):
bpy.ops.wm.open_mainfile(filepath=path)
meshes = sorted((ob for ob in bpy.data.objects if ob.type == 'MESH'), key=lambda ob: ob.name)
print(f"fixture_file: {path}")
print(f"objects_total: {len(bpy.data.objects)}")
print(f"mesh_objects: {len(meshes)}")
verts_total = 0
polys_total = 0
rows = []
for ob in meshes:
verts = len(ob.data.vertices)
polys = len(ob.data.polygons)
verts_total += verts
polys_total += polys
print(f"OBJECT {ob.name} verts={verts} polys={polys} particle_systems={len(ob.particle_systems)}")
for ps in ob.particle_systems:
rows.append((ob.name, ps))
systems_total = 0
emitter_systems = 0
hair_systems = 0
for ob_name, ps in sorted(rows, key=lambda row: (row[0], row[1].name)):
s = ps.settings
systems_total += 1
if s.type == 'EMITTER':
emitter_systems += 1
if s.type == 'HAIR':
hair_systems += 1
inst_name = s.instance_object.name if s.instance_object else "-"
print(f"PARTICLE object={ob_name} system={ps.name} type={s.type} count={s.count} seed={ps.seed} render_type={s.render_type} instance_object={inst_name}")
if s.type == 'EMITTER':
print(f"EMITTER object={ob_name} frame_start={s.frame_start:.1f} frame_end={s.frame_end:.1f} lifetime={s.lifetime:.1f} lifetime_random={s.lifetime_random:.2f} emit_from={s.emit_from} distribution={s.distribution} physics_type={s.physics_type} normal_factor={s.normal_factor:.1f} factor_random={s.factor_random:.1f} particle_size={s.particle_size:.2f} gravity={s.effector_weights.gravity:.1f}")
if s.type == 'HAIR':
print(f"HAIR object={ob_name} hair_length={s.hair_length:.1f} hair_step={s.hair_step} emit_from={s.emit_from} child_type={s.child_type}")
print(f"vertices_total: {verts_total}")
print(f"polygons_total: {polys_total}")
print(f"particle_systems_total: {systems_total}")
print(f"emitter_systems: {emitter_systems}")
print(f"hair_systems: {hair_systems}")
ok = len(meshes) == 3 and systems_total == 2 and emitter_systems == 1 and hair_systems == 1
print(f"verdict: {'PASS' if ok else 'REVIEW'}")
return 0
def main():
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if len(args) == 2 and args[0] == "--make-fixture":
return build_fixture(args[1])
if len(args) == 1:
return audit(args[0])
print("usage: particle_audit.py -- [--make-fixture FILE.blend | FILE.blend]")
return 2
if __name__ == "__main__":
raise SystemExit(main())
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.