enter image description here
I've made a simple snowfall particle simulation (using gandalf3's solution: How to make particles fall like snow). Is there a way to loop the animation, so the transition between last frame and first frame is not visible? Is there a setting to do it?

  • $\begingroup$ one way would be to separate each particle and create a loop animation for it (falling from the top to bottom of image) than combine the loops with different offsets, if particles are a lot ,only separate each group on one plane (which don't overlap ) $\endgroup$
    – Chebhou
    Commented Mar 25, 2015 at 20:12
  • 4
    $\begingroup$ @gandalf3 I'm not sure that the answer to the possible duplicate you point to is really the solution, given that the OP is relying on slow moving particles and not that many of them to hide the start and ending point... $\endgroup$
    – user1853
    Commented Mar 25, 2015 at 20:40

3 Answers 3


All steps needed :

  • render a simple animation loop
  • use the result as a texture for planes
  • each plane has different UV mapping and image sequence offset

Preparing the texture
first we have to prepare a simple animation loop in this case snow flakes falling from top of the image to the bottom ; for this we use the proposed system by gandalf3 and render the particles only from frame 0 to the frame where all flakes disappear from camera, we will use transparent background for this :

the first setup for preparing the textures :

enter image description here

result image sequence (transparency can't be displayed here ) enter image description here

final setup
create planes and add a material with the following setup : enter image description here the image texture is the result of the previous render ,the mapping node is used to create a shift on the X_axis (left right ) while the starting frame on the image sequence texture is used for controlling the Y axis shift (up down )
instance this material for each plane and give it a random values for the shift ,then arrange and resize the planes to fit the camera border from the camera view
second setup for final render
enter image description here

now render this layer for the same number of frames as the last one and you'll get one animation loop as this :

enter image description here

the .blend file

  • $\begingroup$ You could flip some of the layers for variety... $\endgroup$
    – user1853
    Commented Mar 27, 2015 at 4:32
  • 1
    $\begingroup$ @cegaton nice idea, it could be done with setting the scale to -1 in the mapping node $\endgroup$
    – Chebhou
    Commented Mar 27, 2015 at 9:57
  • $\begingroup$ @Gonzou i have added the file $\endgroup$
    – Chebhou
    Commented Mar 27, 2015 at 13:22

A more "clean" way to create a loop using particles:

let's say the render range is 300

Create ParticleSystem

  • Start: -150 End: 150 Life: 300
  • Remove randomness
  • Newtonian physics type
  • object, force etc..

Create ParticleSystem 2 (same object)

  • new data block based on ParticleSystem (everything same as the first system)
  • Start: 150 End: 450 Life: 300

Change numbers according to render range and remove a frame from the beginning or end of the range.

Happy Blendering :)

Heres a demo: https://www.dropbox.com/s/ipp86jqzoeiqn04/SeamlesParticleLoop.blend?dl=1

  • $\begingroup$ Thanks for the method. Could you please explain WHY this works? I mean this is just another simulation for another (later) time range. But what in here makes the last frame be the same as the first one? Thanks again. $\endgroup$
    – hypers
    Commented Dec 24, 2017 at 13:57
  • 4
    $\begingroup$ Using two identical Particle systems will seamlessly continue over to the next particle system that becomes visible. The first particle system is already starting halfway done so the next one that becomes visible should just have to simulate half of the first one. Since the particle systems are identical and no randomness the life of the particles will be the same so born and died in the same place. (You can use the seed if you need to add randomness to the locations) $\endgroup$
    – RMarkus
    Commented Dec 26, 2017 at 22:46
  • $\begingroup$ just a great idea, simple, effective and works like a charm! $\endgroup$
    – Chris
    Commented May 18, 2022 at 8:26

Blender as of 2.80 does not include a built-in feature to loop a particle system. I have conducted some research into this area and although it is possible to custom-craft an external particle system cache that loops, there are several shortcomings in blender's workflow that can either destroy the files, or disobey the data in them.

Start with this "simple" example from http://web.purplefrog.com/~thoth/blender/python-cookbook/particle_loop.html

import struct
import os
import random
from math import *

class BPhysWriter:
    # BPHYS_DATA_* from DNA_object_force.h
    column_words = (1,3,3,4,3,1,3,5)
    column_formats = ("i", "fff", "fff", "ffff", "fff", "f", "fff", "fffff")

    def __init__(self, f, flags):
        self.f = f
        f.write(struct.pack("iii", flavor, self.count, flags))

        self.format = ""
        for i in range(len(BPhysWriter.column_words)):
            if 0 != (flags & (1<<i)):
                self.format += BPhysWriter.column_formats[i]

    def store(self, *elements):
        f = self.f
        f.write(struct.pack("i", self.count))

        f.write(struct.pack(self.format, *elements))

# http://blender.stackexchange.com/questions/15577/how-to-extract-convert-data-from-blender-cache-files-bphys-into-a-human-readabl

class CacheRowReader:
    # BPHYS_DATA_* from DNA_object_force.h
    column_words = (1,3,3,4,3,1,3,5)
    column_formats = ("i", "fff", "fff", "ffff", "fff", "f", "fff", "fffff")

    def __init__(self, flavor, count, data_type_flags):
        self.count = count
        self.flavor = flavor
        self.data_type_flags = data_type_flags

        unpack_format = ""
        for i in range(len(CacheRowReader.column_words)):
            if 0 != (data_type_flags&(1<<i)):
                rec_len += 4 * CacheRowReader.column_words[i]
                unpack_format += CacheRowReader.column_formats[i]

        self.rec_len = rec_len
        self.unpack_format = unpack_format

    def parse(cls, f):

        magic = f.read(8)
        if magic != b'BPHYSICS':
            raise Exception("not a blender physics cache")

        flavor = f.read(12)
        (flavor,count,data_type_flags) = struct.unpack("iii", flavor)

        #print( "%d\t%d\t%d"%(flavor,count,data_type_flags))

        return CacheRowReader(flavor, count, data_type_flags)

    def read_row(self, f):
        :param f: a file opened with "rb" (binary) mode
        raw = f.read(self.rec_len)

        if raw is None or len(raw)==0:
            return None
        if len(raw) != self.rec_len:
            raise Exception("short read (%d<%d)"%( len(raw), self.rec_len))

        columns = struct.unpack(self.unpack_format, raw)

        rval = dict()
        cursor =0
        # a lot of these clauses are untested.  Feel free to leave me a comment on the stackexchange answer.
        if 0 != (self.data_type_flags&1):
            rval['index'] = columns[cursor]
            cursor +=1
        if 0 != self.data_type_flags&2:
            rval['location'] = columns[cursor:cursor+3]
            rval['smoke_low'] = rval['location']
            cursor +=3
        if 0 != self.data_type_flags&4:
            rval['velocity'] = columns[cursor:cursor+3]
            rval['smoke_high'] = rval['velocity']
            cursor +=3
        if 0 != self.data_type_flags&8:
            rval['dynamicpaint'] = rval['rotation'] = columns[cursor:cursor+4]
            cursor +=4
        if 0 != self.data_type_flags&0x10:
            rval['xconst'] = rval['avelocity'] = columns[cursor:cursor+3]
            cursor +=3
        if 0 != self.data_type_flags&0x20:
            rval['size'] = columns[cursor]
            cursor +=1
        if 0 != self.data_type_flags&0x40:
            rval['times'] = columns[cursor:cursor+3]
            cursor +=3
        if 0 != self.data_type_flags&0x80:
            rval['boids'] = columns[cursor:cursor+5]
            cursor +=5

        return rval


def mission1():
    cache_pattern = "blendcache_particle-loop/synth_%06d_00.bphys"

    create_particle_cache(cache_pattern, 1, 250)

nrows = ncols
particleCount = ncols*nrows

def locationForParticle(pnum, frame):

    u = pnum%ncols
    v = floor(pnum/ncols)
    x = (u/ncols -0.5)*8
    y = (v/nrows -0.5)*8

    r = sqrt(x*x+y*y)
    loopPeriodFrames = 250
    phase1 = 2*pi * frame/loopPeriodFrames
    z = 0.2*cos(r*5+phase1*5)

    return x,y,z

def create_particle_cache(cache_pattern, frame_start, frame_end):

    fpattern = cache_pattern

    with open(fpattern % 0, "wb") as out:
        phys = BPhysWriter(out, 0x40)
        # XXX blender 2.80 does not fully respect this file
        # and will rewrite the particle timings if
        # you use Alt-A to play long enough to reach the end and start a second play cycle.

        life = 9999
        for i in range(particleCount):
            tm = i / (10 * particleCount)
            phys.store(tm, tm + life, life)

    for frame in range(frame_start, frame_end + 1):
        fname = fpattern % frame
        with open(fname, "wb") as out:
            phys = BPhysWriter(out, 3)
            good_count = 0
            for idx in range(particleCount):
                x, y, z = locationForParticle(idx, frame)
                if (frame == frame_start):
                    print(idx, x, y, z)
                phys.store(idx, x, y, z)
            print("wrote %s"%fname)

def set_pad(list, idx, val):
    """list[idx] = val
    even if list is short, we'll pad it with None-s until we can fit the value in the correct slot"""
    while len(list) < idx:
    list.insert(idx, val)



To use the generated particle system cache you have to adjust the settings to point at it: particle system settings to use external cache

The name of the particle system in the list defaults to being blank, but I double-clicked and changed it to synth. I also checked the External button, and set the File path to the directory where the cache files live.

The first shortcoming in blender's logic is that it does not actually obey the timing information in the *_000000_00.bpys file. It will look good the first time you play it in the editor, but if you allow the animation to reach the end and loop back to the beginning blender will recalculate all the particle timings according to the settings in the particle system.

And it will discard the timings you created when it comes time to render.

To work around blender's failure to respect the timings the easiest way is to set both the start and end of the particle system to the start frame of the particle simulation, and generate your particle cache with all particles matching that frame, and the same lifetime that you use in the particle cache. Since this does not accommodate particles that are born or die during the animation you will have to compensate by locating those particles off-camera for frames when you want them to be "dead".

timing settings

If you do not do this before you rig the external cache you will discover another defect in blender. It wipes your cache during the steps necessary to re-enable the panel containing the Emission settings

  • uncheck External
  • uncheck Disk Cache
  • click Free Bake button

and now the files you have constructed have been deleted by blender and you must regenerate them.

The code above generates the looping particle cache using a (relatively) simple formula for particle coordinates. It is also possible to create the looping particle cache by copying data from a particle cache generated by blender's built-in capabilities, but I haven't cleaned up that example to a state where I can publish it.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .