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$– ChebhouMar 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$– user1853Mar 25, 2015 at 20:40
3 Answers
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 )
final setup
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 :
-
$\begingroup$ You could flip some of the layers for variety... $\endgroup$– user1853Mar 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$– ChebhouMar 27, 2015 at 9:57
-
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$– hypersDec 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$– RMarkusDec 26, 2017 at 22:46
-
$\begingroup$ just a great idea, simple, effective and works like a charm! $\endgroup$– ChrisMay 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):
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()
To use the generated particle system cache you have to adjust the settings to point at it:
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.