Skip to content

Instantly share code, notes, and snippets.

@fm7521

fm7521/bphys.py Secret

Created May 31, 2022 18:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fm7521/572aa51de85f4dfe5d9c2ffc18358a56 to your computer and use it in GitHub Desktop.
Save fm7521/572aa51de85f4dfe5d9c2ffc18358a56 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
"""
A frankenscript that converts baked softbody physics data to a pc2 file
Tested in Blender 3.3 2a2261d7 Linux x86_64
Instructions:
1. Create desired physics animation using a lattice and softbody physics modifier.
2. Bake the animation and enable "save to disk". Note the folder named blendcache_library.
3. Record the x y z scale of the lattice in a file (e.g. named lattice_scale) One dimension per line
4. Run this script: python3 baked_physics_to_pc2.py < lattice_scale
5. Create a new lattice with the same scale and dimensions
6. Add a Mesh cache modifier to the lattice (don't have physics) and use the .pc2 file outputted by the script
"""
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# SOURCE: https://blenderartists.org/t/point-cache-doctor-script/593708
import os, struct, sys, argparse
from sys import argv, flags
from os import path
_byteorder = 'little'
_byteorder_fmt = '<'
def dict_merge(d, other):
"""Utility function for creating a modified dict"""
c = d.copy()
c.update(other)
return c
def print_progress_bar(msg, percent, size):
sys.stdout.write('{0}\r[{1}] {2}%'.format(msg, ('#'*int(percent/100.0*size)).ljust(size), percent))
sys.stdout.flush()
def cache_files(directory, index=0):
"""Cache frame files in a directory"""
for filename in os.listdir(directory):
try:
base, ext = path.splitext(filename)
parts = base.split('_')
if len(parts) in (2, 3):
cframe = int(parts[1])
cindex = int(parts[2]) if len(parts) >= 3 else 0
if cindex == index:
yield cframe, filename
except:
pass
def cache_file_list(directory, index=0):
"""Cache frame files in a directory, sorted by frame"""
return sorted(cache_files(directory, index), key=lambda item: item[0])
def pack_string(v, size):
return struct.pack('%ds' % size, v.encode(encoding='UTF-8'))
def unpack_string(b, size):
return struct.unpack('%ds' % size, b)[0].decode(encoding='UTF-8')
def pack_uint(v):
return struct.pack('I', v)
def unpack_uint(b):
return struct.unpack('I', b)[0]
def pack_float(v):
return struct.pack('f', v)
def unpack_float(b):
return struct.unpack('f', b)[0]
def pack_vector(v):
return struct.pack('fff', v[0], v[1], v[2])
def unpack_vector(b):
return struct.unpack('fff', b)
def pack_quaternion(v):
return struct.pack('ffff', v[0], v[1], v[2], v[3])
def unpack_quaternion(b):
return struct.unpack('ffff', b)
def pack_color(v):
return struct.pack('ffff', v[0], v[1], v[2], v[3])
def unpack_color(b):
return struct.unpack('ffff', b)
class ParticleTimes():
__slots__ = ('birthtime', 'lifetime', 'dietime')
def __init__(self, birthtime, lifetime, dietime):
self.birthtime = birthtime
self.lifetime = lifetime
self.dietime = dietime
def pack_particle_times(v):
return struct.pack('fff', v.birthtime, v.lifetime, v.dietime)
def unpack_particle_times(b):
birthtime, lifetime, dietime = struct.unpack('fff')
return ParticleTimes(birthtime, lifetime, dietime)
class BoidData():
__slots__ = ('health', 'acceleration', 'state_id', 'mode')
def __init__(self, health, acceleration, state_id, mode):
self.health = health
self.acceleration = acceleration
self.state_id = state_id
self.mode = mode
def pack_boid(v):
return struct.pack('ffffhh', v.health, v.acceleration[0], v.acceleration[1], v.acceleration[2], v.state_id, v.mode)
def unpack_boid(b):
health, acc0, acc1, acc2, state_id, mode = struct.unpack('ffffhh')
return BoidData(health, (acc0, acc1, acc2), state_id, mode)
class TypeDesc():
"""Data type descriptor"""
def __init__(self, index, name, size, pack, unpack):
self.index = index
self.name = name
self.size = size
self.pack = pack
self.unpack = unpack
def __str__(self):
return self.name
def __repr__(self):
return "TypeDesc(name=%r, size=%d)" % (self.name, self.size)
_data_types_softbody = (
TypeDesc(1, 'LOCATION', 12, pack_vector, unpack_vector),
TypeDesc(2, 'VELOCITY', 12, pack_vector, unpack_vector),
)
_data_types_particles = (
TypeDesc(0, 'INDEX', 4, pack_uint, unpack_uint),
TypeDesc(1, 'LOCATION', 12, pack_vector, unpack_vector),
TypeDesc(2, 'VELOCITY', 12, pack_vector, unpack_vector),
TypeDesc(3, 'ROTATION', 16, pack_quaternion, unpack_quaternion),
TypeDesc(4, 'AVELOCITY', 12, pack_vector, unpack_vector),
TypeDesc(5, 'SIZE', 4, pack_float, unpack_float),
TypeDesc(6, 'TIMES', 12, pack_particle_times, unpack_particle_times),
TypeDesc(7, 'BOIDS', 20, pack_boid, unpack_boid),
)
_data_types_cloth = (
TypeDesc(1, 'LOCATION', 12, pack_vector, unpack_vector),
TypeDesc(2, 'VELOCITY', 12, pack_vector, unpack_vector),
TypeDesc(4, 'XCONST', 12, pack_vector, unpack_vector),
)
_data_types_smoke = (
TypeDesc(1, 'SMOKE_LOW', 12, pack_vector, unpack_vector),
TypeDesc(2, 'SMOKE_HIGH', 12, pack_vector, unpack_vector),
)
_data_types_dynamicpaint = (
TypeDesc(3, 'DYNAMICPAINT', 16, pack_color, unpack_color),
)
_data_types_rigidbody = (
TypeDesc(1, 'LOCATION', 12, pack_vector, unpack_vector),
TypeDesc(3, 'ROTATION', 16, pack_quaternion, unpack_quaternion),
)
_type_map = {
0 : ('SOFTBODY', _data_types_softbody),
1 : ('PARTICLES', _data_types_particles),
2 : ('CLOTH', _data_types_cloth),
3 : ('SMOKE_DOMAIN', _data_types_smoke),
4 : ('SMOKE_HIGHRES', _data_types_smoke),
5 : ('DYNAMICPAINT', _data_types_dynamicpaint),
6 : ('RIGIDBODY', _data_types_rigidbody),
}
_flag_map = {
0x00010000 : 'compress',
0x00020000 : 'extra_data',
}
class CacheFrame():
def __init__(self, filename):
self.filename = filename
self.totpoint = 0
self.data_types = tuple()
self.data = tuple()
def get_data_type(self, name):
for dt in self.data_types:
if dt.name == name:
return dt
return None
def get_data(self, name):
for dt, data in zip(self.data_types, self.data):
if dt.name == name:
return data
return None
def set_data(self, name, values):
self.data = tuple(data if dt.name != name else values for dt, data in zip(self.data_types, self.data))
def read(self, directory, read_data):
cachetype = ""
data_types = {}
f = open(path.join(directory, self.filename), "rb")
try:
cachetype, data_types = self.read_header(f)
if read_data:
self.read_points(f)
else:
self.data = None
finally:
f.close()
return cachetype, data_types
def read_header(self, f):
bphysics = unpack_string(f.read(8), 8)
if bphysics != 'BPHYSICS':
raise Exception("Not a valid BPHYSICS cache file")
typeflag = unpack_uint(f.read(4))
cachetype, data_types = _type_map[typeflag & 0x0000FFFF]
self.cachetype = cachetype
for bits, flag in _flag_map.items():
setattr(self, flag, bool(typeflag & bits))
self.totpoint = unpack_uint(f.read(4))
data_types_flag = unpack_uint(f.read(4))
# frame has filtered data types list in case not all data types are stored
self.data_types = tuple(filter(lambda dt: ((1<<dt.index) & data_types_flag) != 0, data_types))
return cachetype, data_types
def read_points(self, f):
data = tuple([None] * self.totpoint for dt in self.data_types)
if self.compress:
raise Exception("Compressed caches are not supported yet, sorry ...")
return data
def interleaved():
for k in range(self.totpoint):
yield k, tuple(dt.unpack(f.read(dt.size)) if dt else None for dt in self.data_types)
for k, data_point in interleaved():
for data_list, value in zip(data, data_point):
data_list[k] = value
self.data = data
def write(self, directory):
f = open(path.join(directory, self.filename), "wb")
try:
self.write_header(f)
self.write_points(f)
finally:
f.close()
def write_header(self, f):
f.write(pack_string('BPHYSICS', 8))
typeflag = 0
for index, (name, _) in _type_map.items():
if name == self.cachetype:
typeflag = typeflag | index
break
for bits, flag in _flag_map.items():
if getattr(self, flag):
typeflag = typeflag | bits
f.write(pack_uint(typeflag))
f.write(pack_uint(self.totpoint))
data_types_flag = 0
for dt in self.data_types:
data_types_flag = data_types_flag | (1<<dt.index)
f.write(pack_uint(data_types_flag))
def write_points(self, f):
if self.compress:
raise Exception("Compressed caches are not supported yet, sorry ...")
return data
for k in range(self.totpoint):
for data, dt in zip(self.data, self.data_types):
f.write(dt.pack(data[k]))
class PointCache():
def __init__(self, directory, index=0):
self.files = cache_file_list(directory, index)
if not self.files:
raise Exception("No point cache files for index %d in directory %s" % (index, directory))
self.start_frame, info_filename = self.files[0]
self.end_frame, _ = self.files[-1]
info_frame = CacheFrame(info_filename)
cachetype, data_types = info_frame.read(directory, read_data=False)
self.cachetype = cachetype
self.data_types = data_types
for flag in _flag_map.values():
setattr(self, flag, getattr(info_frame, flag))
self.totpoint = info_frame.totpoint
def get_data_type(self, name):
for dt in self.data_types:
if dt.name == name:
return dt
return None
def cache_copy(idir, odir, index):
cache = PointCache(idir, index)
for cfra, filename in cache.files:
frame = CacheFrame(filename)
frame.read(idir, read_data=True)
frame.write(odir)
#SOURCE: https://github.com/joshhale/blender-addons/blob/master/io_export_pc2.py
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
from os import remove
import time
import math
import struct
def get_sampled_frames(start, end, sampling):
return [math.modf(start + x * sampling) for x in range(int((end - start) / sampling) + 1)]
def export_pc2(point_cache: PointCache, cache_frames: list[CacheFrame], filepath, scale):
start = point_cache.start_frame
vertCount = len(cache_frames[0].data[0])
headerFormat='<12siiffi'
headerStr = struct.pack(headerFormat, b'POINTCACHE2\0', 1, vertCount, start, 1, vertCount)
with open(filepath, "wb") as fo:
fo.write(headerStr)
for frame in cache_frames:
locations = frame.data[0]
for vert in locations:
vert = tuple(c * (1/s) for c, s in zip(vert, scale))
fo.write(struct.pack("<fff", *vert))
def main(folder="blendcache_library", out_file="baked.pc2", scale=None):
if scale is None:
scale = (float(input("enter scale_x ")), float(input("enter scale_y ")), float(input("enter scale_z ")))
pc = PointCache(folder)
cache_frames = []
for frame_num, file in pc.files:
frame = CacheFrame(file)
descriptor = frame.read(folder, True)
assert descriptor[0] == "SOFTBODY" and descriptor[1][0].name == "LOCATION", "Unexpected format!"
locations = frame.data[0]
# print(f"Got {len(locations)} locations! at frame {frame_num}")
cache_frames.append(frame)
export_pc2(pc, cache_frames, out_file, scale)
if __name__ == "__main__" and not flags.interactive:
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment