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?
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 :
result image sequence (transparency can't be displayed here )
create planes and add a material with the following setup : 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
now render this layer for the same number of frames as the last one and you'll get one animation loop as this :
A more "clean" way to create a loop using particles:
let's say the render range is 300
- 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 :)
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): f.write(b"BPHYSICS") self.f = f self.count=0 flavor=1 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): i=self.count f = self.f self.count+=1 f.seek(12) f.write(struct.pack("i", self.count)) f.seek(0,2) 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 rec_len=0 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 @classmethod 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 :return: """ 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) ncols=40 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.append(None) list.insert(idx, val) # # # mission1()
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".
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.