6
$\begingroup$

I am using a Python script to render the below scene in Blender. Before Packing

Now, I am trying to pack them using the piston on the right hand side to make it look like this:

After Packing

The center of mass before packing will be roughly at the center of the box and it will be shifted to a different point after packing the objects. The question I have is, how to do the center of mass drawing in the Blender and how to calculate it in the Blender using a Python script. Does Blender have an inbuilt feature for doing this? I have checked online and the closest I have found is this.

http://www.wildpockets.com/sites/all/themes/ninesixty/exporterstemp/wildPocketsBlenderExporter-v0.6.py

Additional Information:

  • Using Blender 2.74 on OsX
  • Using Blender Game Engine to do the packing
$\endgroup$

2 Answers 2

4
$\begingroup$

A bit late to answer this, but I recently had to compute the center of gravity of various objects, so here is what I got.

EDIT: and unfortunately I wrote this answer shortly before a major Blender API overhaul, which forced me to rewrite it completely when someone showed some interest in my little hack, 7 years later.
Oh well, you can't have too much of a delicious language like Python...

Rationale

Though it was not available at the time the OP asked, Blender has long added the possibility to set the origin of an object to the center of its volume.
In object mode, right click on an object, set origin -> origin to the center of mass (volume) and Bob will be your uncle.
If you only want to work on a single object, you can stop reading this wall of text right now and live happily ever after.

Unfortunately, this feature does not help much when computing the CG of a bunch of different objects, since you also need their weights, about which Blender has no clue.

The first idea is to let Blender know about the weight of individual objects, by defining a weight custom property for each of them. A script can then easily compute the CG of a collection of objects based on their weights and positions.

However, defining the individual weights of every bit of wood, canvas or rope in your galleon project would be beyond impractical. Besides, you would need to update their weights each time you modified them and changed their size.

The idea is rather to define the densities of the materials your objects are made of and let Blender compute their weights based on their geometry. Changes in the geometry are immediately reflected in the object weight with no extra work, and the number of required materials should not be huge.

Also, there are different ways to define a density.
Volumetric density works well for massive objects like stone blocks, but other objects are made of thin sheets (a cardboard box, the sail of a boat) and would rather be given a surface density. Others are thread-shaped (a rope or a cable), and better associated with a linear density.

Basic principle

To make an object heavy, you will need to define

  • a weight custom property at object level, to represent the mass of a complex object like a car engine, or

  • a custom property in the object active material, that can be :

    • density for solid objects of constant density, like a bowling ball or a stone block
    • surface density for flat objects like pieces of cloth or sheet metal
    • linear density for thread-shaped objects like ropes or rods

When the weight property is used, the object geometry becomes purely decorative. Notably, the script will not attempt to compute the object's CG even if it has a density defined in its active material. The object's origin will be used instead.
You can use Blender's features to position it at the center of faces or volume, or anywhere you fancy.
Empties can be used for placeholders or if you prefer to leave the objects weightless and keep weight definitions separate.

As for "dense" objects, once densities have been defined in your basic set of materials, you have nothing special to do.
Apply a material that defines an appropriate density to any object that has a geometry, and voilà, you got yourself a weighty object.

Once the objects have been given a weight, you can compute the CG of any or all of them as follows:

  • select all the objects you want to include in the computation

Hidden objects will be ignored by default, but that is configurable.
You can safely include "weightless" objects in the selection, they will simply be skipped.
A "select all" command will allow to compute the CG of all visible objects in the scene.

  • launch the script

The computation can take a while on big projects but should be instantaneous on a simple test scene.

You should see the cursor jump to the computed CG location. If you open the console, you'll see the coordinates of the CG and the total weight of the selected objects.

You might also, alas, get errors. The CG will always be computed unless no valid object is found, but some objects might be excluded from the computation, depending on the issues that lead the script to reject them, producing a more or less invalid result.

Example

This RC plane uses volumetric density for balsa and 3D-printable plastic parts, surface density for wing and fuselage covering, linear density for push rods and individual weights for components like servos, battery or motor.
It has well over a hundred parts of all kinds of shapes, but density definitions are limited to a few materials (balsa, plastic, covering, carbon rod, piano wire, etc) and weight definitions to a handful of components.

It acts also as a nice benchmark, with about 250 "massive" objects, 600K faces and 1,2M triangles. The script takes about 3 seconds to process it on my middle-range PC.

See how the CG moves: battery front Battery placed further back battery back Landing gear down gear down

Design choices

The general idea is to try and keep things simple, so I went for a WISIWYG approach: the geometry used for computation is what you see on the screen.

It's quite efficient, since the vast majority of the meshes are already computed by Blender to display the scene. Only the curves with a linear density require Blender to do a bit of extra computation, but they are typically far less complex than subdivided surfaces.

A significant optimization (it makes the computation 2 or 3 times faster) requires a big-ish buffer and the special case of curves leads to the creation of temporary objects, but the extra memory needed is only a fraction of what the objects already consume.

I enforced a few limitations to (hopefully) make the script safer to use.
For instance, you can't define two different densities (say, both linear and volumetric) in the same material. Though it could work in some cases, it creates ambiguities that could lead to puzzling problems. Better frustrated and safe than free to be sorry.

The script only has a handful of options that should not give anyone a headache. It has zero user interface though, so you'll have to edit the parameters section located in the header of the code to configure it.

A few problems and how the script handles them

  1. picking the right kind of density

Only some kinds of densities will make sense for a given object. For instance, a pure wire frame has neither a volume nor a surface. Only linear density makes sense in that case.
Conversely, metaballs will always define a volume, so giving them a linear density makes no sense.

In some cases the object type is not enough. Meshes an curves can define volumes, surface or mere lines. The last resort rule is that an object that generates no faces can only be given a linear density, while others can only have surface or volumetric density.

If an object breaks any of these rules, it will be skipped and you'll get an error message.

  1. water tightness

It is well known that only watertight shapes (very similar to manifold, only a bit more permissive) can properly be given a volume. If a shape is "leaky", it has no interior and its volume cannot be (mathematically) defined.

In practice, you cannot reliably compute a volume before you locate the gaps and plug them. A given algorithm might produce some number, but it might have little or no meaning.

Detecting a leaky shape is not a trivial problem. In my case, I use a very simple heuristic that seems to spot leaky objects pretty well, but it might theoretically fail to detect some.

The volume computation method used by the script is fast and requires the bare minimum of geometry data, but can produce very wrong results on leaky shapes.

The script will report the objects it considers to be leaky, but there is simply no known algorithm that can fix a leaky mesh, let alone this little piece of code. You'll be on your own to find and plug the holes.

You might want to try the 3D print add-on in the standard Blender distribution. It helps a lot with wacky meshes.

  1. the case of curves

Curves are very versatile tools that are often used to produce shapes that don't look a bit like a curve.
The problem is, most curves are actually volumes, since they are given thickness by beveling or extruding a section along their path. If they are converted to meshes, the topology of the underlying curve is lost.
There is a function to compute a curve length somewhere in the Blender API, but the length is not enough, the geometry is also needed to locate the CG.

To address this problem, when a curve is assigned a linear density, the script will ignore all parameters that give it volume (namely the beveling and extrusion) before converting it to a mesh. The result will be a string of segments that can be processed like any other wire frame.

If some curve is meant to represent a complex volume, it can (and should!) be given a surface or volumetric density instead.

  1. units

The script has no notion of weight, only of "some unit of weight per length/surface/volume". When the script says the weight of an object is 10, only the user knows whether it's pounds or kilos.
The only thing that matters is defining a common measurement for distances, to get homogeneous results across linear, surface and volumetric densities.

When creating a new scene, you'll have to decide what units to use and configure the script before starting to sprinkle density definitions everywhere.

Configuring the script is an unavoidable step. Even though Blender allows some unit definitions in the scene parameters, it has no notion of density and offers no settings for that.

I've added a modicum of help to configure the script for using a set of units (one for each type of density). This might spare you the hassle to lookup the way Blender stores coordinates internally, but it's really no rocket science.

  1. allowed objects

All types of objects that have a geometry are supported (namely curves, surfaces, meshes and metaballs), but I limited geometry-less objects to empties.
Technically you could define a weight for, say, a camera, but there are already enough ways to turn a scene into a sorry mess without adding more.

Some extras

  • You can specify the name of an object that will serve as CG marker instead of the cursor

  • You can ask for a report of all "massive" objects and their associated materials (displayed in Blender's console). That can be handy to get things organized or sort out mishaps like typos or wrong clicks.

A few tips

  • If some object proves to be leaky beyond repair, you can give it an individual weight and set its origin to the center of volume (or by hand if everything else fails). Once it gets its own private weight, the script will no longer attempt (and fail) to compute its volume.
  • Giving curves a surface or volumetric density can be very costly. Default subdivision settings generate a lot of polygons. Volumetric density is especially tricky. Generated meshes can easily be leaky (intersecting faces can be generated when a curve takes a sharp bend or loops across itself). And don't forget to cap the ends :)

The code

Beside my little RC plane, I tested it on massive meshes. The biggest I could create before Blender ran out of memory and crashed to desktop was about 16M triangles. It took more than one minute to process that monster, but my cat survived.

Still, it's just a little hack, nowhere near the quality of a professional tool.
I tried to make it usable by non-programmers, but it's still bare bone Python with no user interface.
And to top it all, I'm anything but a Python specialist.

Anyway, revisiting this code after 7 years was fun. All the better if other people find some use for it.

import bpy
import time
from enum import Flag,auto
from mathutils import Vector

# ---------------------------------------
# some parameters you might want to tweak
# ---------------------------------------

# Don't generate an exception when errors are detected. Enable at your own risk
Quiet = False

# Print a list of all density-defining materials and associated massive objects
# Temporarily enabling this option might help locating your "massive" objects, organizing your materials or sorting out typos.
Report=False

# Ignore hidden objects even when they are selected. Can be safely disabled, at your convenience
SkipHiddenObjects = True

# Use a dedicated object to mark the CG position
# If the object is not found, the cursor will be moved instead
CGMarkerName="CG marker"

# ---------------------------------------
# units
# ---------------------------------------

#   As a convenience, a dictionary of metric and imperial units is provided to make the definitions less painful
#   The only requirement is to keep the same weight unit for all densities. If you want to do more fancy stuff, you'll
# have to define the conversion coefficients yourself.
#   For instance this will set units to pounds, pounds/yard, pounds/square inch and pounds/cubic foot
# (assuming you read the "10.5" displayed by the script as "10.5 pounds")
#LinearUnit     = "yd"
#SurfaceUnit    = "in"
#VolumetricUnit = "ft"

LinearUnit     = "m"   
SurfaceUnit    = "dm"
VolumetricUnit = "cm"

Units = { # The coefficients are simply units expressed in meters
    # metric
    "km":1000,"hm":100,"dam":10, "m" : 1, "dm" : 0.1, "cm":0.01, "mm":0.001, "um":0.000001,
    # imperial
    "mi" :1609.344, # mile
    "fur":2012.168, # furlong
    "ch" :20.1168,  # chain
    "yd" :0.9144,   # yard
    "ft" :0.3048,   # foot
    "in" :0.0254,   # inch
    "mil":0.0000254 # thou
}

# You can chuck my little list of units out the window and set these coefficients directly, if you know what you're doing
scaling = bpy.context.scene.unit_settings.scale_length
ConvertLength  = pow (Units[LinearUnit    ] / scaling,-1)
ConvertSurface = pow (Units[SurfaceUnit   ] / scaling,-2)
ConvertVolume  = pow (Units[VolumetricUnit] / scaling,-3)

# ------------------------------------------------------------------------------------------------
# The code proper. Tinker at your own risk
# ------------------------------------------------------------------------------------------------

failure = False 
def croak (*args, **kwargs):
    global failure # hopefully not!
    print ("!?!", *args, *kwargs)
    failure = True

class Density (Flag):
    Punctual   = auto()
    Linear     = auto()
    Surface    = auto()
    Volumetric = auto()
Weightless = Density(0)

ValidDensities = {
    'EMPTY'   : Density.Punctual,
    'SURFACE' : Density.Punctual |                  Density.Surface,
    'CURVE'   : Density.Punctual | Density.Linear | Density.Surface | Density.Volumetric,
    'META'    : Density.Punctual |                  Density.Surface | Density.Volumetric,
    'MESH'    : Density.Punctual | Density.Linear | Density.Surface | Density.Volumetric
    }

def triangles (polygon):
    """enumerate triangles in a face"""
    # Loop triangles are not available for visualization meshes, we need to use polygonal faces instead
    for i in range (1, len(polygon)-1):
        yield (polygon[0], polygon[i], polygon[i+1])

def scale_vertices (vertices, scale):
    """ scale object coordinates (according to world matrix) """
    # pre-scaling vertices is more than 2.5 times faster, for a reasonable memory cost
    # lists appear to be slightly faster than tuples in that case
    return [Vector (x * y for x,y in zip (vertex.co,scale)) for vertex in vertices]

def mesh_length (mesh,scale):
    """center of gravity and mass of a mesh seen as a wire frame (a collection of edges with no faces)"""
    center = Vector()
    length = 0
    scaled_vertices = scale_vertices (mesh.vertices, scale)
    for segment in mesh.edges:
        a, b = (scaled_vertices[v] for v in segment.vertices)
        l = (a-b).length        # segment length
        center += l * (a+b)     # 2 x middle point, weighted by length
        length += l
    if length != 0: # constants moved out of the loop
        center /= length * 2 
    return center, length
    
def mesh_surface (mesh,scale):
    """center of mass of a mesh seen as an infinitely thin surface"""
    center = Vector()
    surface = 0
    scaled_vertices = scale_vertices (mesh.vertices, scale)
    for face in mesh.polygons:
        for triangle in triangles (face.vertices):
            a,b,c = (scaled_vertices[v] for v in triangle)
            s = (b-a).cross(c-a).length # 2 x triangle surface
            center += s * (a+b+c)       # 2 x 3 x center, weighted by surface
            surface += s
    if surface != 0:  # constants moved out of the loop
        center /= surface * 3
        surface /= 2 
    return center, surface
    
def mesh_volume (mesh,scale):
    """center of mass of a mesh seen as a constant density volume (only watertight meshes will produce correct results)"""
    # The algorithm sums signed volumes of tetrahedrons built by adding a fixed reference point to every triangle
    # in the mesh. When the mesh is watertight, the bits of tetrahedrons that lie outside the actual
    # shape end up canceling each other out. When the shape is leaky, bits of tetrahedrons are left sticking
    # out, so to speak, which can produce complete garbage, even with inconspicuous holes.
    #   Leaky shapes are very likely to yield different results when the reference point changes. The heuristic
    # picks a second reference point beside the origin, computes both volumes and looks for discrepancies.
    #   Some leaky meshes could slip under the radar, but it's simple and apparently efficient
    center = Vector()
    volume = 0
    d = mesh.vertices[0].co # pick an arbitrary point from the mesh for water tightness check
    vcheck = 0
    for face in mesh.polygons:
        for triangle in triangles(face.vertices):
            a,b,c = (mesh.vertices[v].co for v in triangle)
            v = a.cross(b).dot(c) # 6 x volume of a tetrahedron with the 4th point at the origin
            center += v * (a+b+c) # 6 x 4 x center, weighted by volume (the 4th point has null coordinates)
            volume += v
            vcheck += (a-d).cross(b-d).dot(c-d) # 6 x volume of a second tetrahedron using another reference point
    vmin = min (volume,vcheck)
    if vmin != 0 and abs(volume-vcheck)/vmin > 0.01: # 1% relative error
        center = None # the caller just cannot overlook this, but it's a terrible way of reporting an error
    else:    
        # contrary to length and surface, volume can be scaled globally without adjusting every single vertex.
        for factor in scale: volume *= factor
        if volume != 0: # constants moved out of the loop
            center /= volume * 4
            volume /= 6 
    return center, volume

def obj_density (obj):
    """retrieve density settings from object and material custom properties"""
    density = Weightless
    factor = 0
    try:
        factor = obj['weight']
        density = Density.Punctual
    except KeyError:
        count = 0
        if obj.active_material != None:
            #print(obj.active_material.name, obj.active_material.keys(), obj.name)
            try:
                factor = obj.active_material['linear density'] * ConvertLength
                density |= Density.Linear
                count+=1
            except KeyError: pass
            try:
                factor = obj.active_material['surface density'] * ConvertSurface
                density |= Density.Surface
                count+=1
            except KeyError: pass
            try:
                factor = obj.active_material['density'] * ConvertVolume
                density |= Density.Volumetric
                count+=1
            except KeyError: pass
            if count > 1 :
                croak ("Multiple densities defined in material", obj.active_material.name, ", skipping object", obj.name)
                return Weightless,0
 
    # check compatibility with object type
    if obj.type in ValidDensities:
        if not density in ValidDensities[obj.type]:
            croak (obj.name,"of type",obj.type,"cannot have a", density.name.lower(), "density")
            return Weightless,0
    else:
        croak (obj.name,"of type",obj.type,"cannot be assigned a weight")
        return Weightless,0
    return density, factor
    
def center_of_gravity_and_weight (obj, density, factor):
    # take care of punctual masses
    if density == Density.Punctual: return obj.location, factor
    
    # prepare object for processing (i.e. strip the curves from their volume when needed)
    if obj.type == 'CURVE' and density == Density.Linear:
        # remove beveling and extrusion for curves with linear weight
        prepared = obj.copy()
        prepared.data = obj.data.copy()
        prepared.data.bevel_depth = 0
        prepared.data.bevel_object = None
        prepared.data.extrude = 0
    else:
        # other objects need no tweaking before applying their modifiers
        prepared = obj
        
    # apply active modifiers
    evaluated = prepared.evaluated_get(depsgraph)
    
    # convert object data to mesh if needed
    if obj.type == 'MESH':
        mesh = evaluated.data
    else:
        mesh = bpy.data.meshes.new_from_object(evaluated)

    # last sanity checks now that we got the final mesh
    center, weight = None, 0
    if not mesh.polygons and not mesh.edges:
        croak (obj.name,"has no computable geometry and cannot be assigned a weight. You might want to use an empty instead")
    elif mesh.polygons and density == Density.Linear:
        croak (obj.name,"has a surface and cannot be assigned a linear weight")
    elif not mesh.polygons and density != Density.Linear:
        croak (obj.name,"is a wire frame and can only be assigned a linear weight")
    else:
        # compute center and weight at last
        center,magnitude = {
            Density.Linear     : mesh_length,
            Density.Surface    : mesh_surface,
            Density.Volumetric : mesh_volume
            }[density](
                mesh,
                obj.matrix_world.to_scale()) # take scaling into account        
        weight = magnitude * factor
        if center == None:
            croak(obj.name, "might not be watertight")
    # cleanup
    if mesh != evaluated.data: bpy.data.meshes.remove(mesh)
    
    # back to world coordinates
    if center == None: center = Vector() # position the CG at local origin if volume computation failed
    return obj.matrix_world @ center, weight

# ------------------------------------------------------------------------------------------------
# Main 
# ------------------------------------------------------------------------------------------------
center = Vector()
weight = 0
# allows to access the representation of a primitive object with active modifiers applied
depsgraph = bpy.context.evaluated_depsgraph_get()

start = time.time()
for obj in bpy.context.selected_objects:
    # optionally skip hidden objects
    if SkipHiddenObjects and obj.hide_render: continue
    
    # get density definitions from object and material custom properties
    density, factor = obj_density (obj)
    if density == Weightless : continue # skip invalid or weightless objects
    
    # attempt CG computation
    c, w = center_of_gravity_and_weight (obj, density, factor)
    center += c * w
    weight += w

if Report:
    # display weighty materials and objects
    weighty_materials = []
    for density in ("linear ","surface ",""):
        print(f"====== {density if density != '' else 'volumetric '}density")
        for mat in bpy.data.materials:
            if not density+"density" in mat: continue
            weighty_materials.append(mat)
            print (f"--- {mat.name} ({mat[density+'density']:.2f})")
            for obj in bpy.data.objects:
                if obj.get("weight") is not None: continue
                if obj.active_material == mat: print (obj.name)
    print("====== individual weights")
    for obj in bpy.data.objects:
        if obj.get("weight") is None: continue
        print (f"{obj.name} ({obj['weight']:.2f})",end="")
        if obj.active_material in weighty_materials:
            print (" overriding", obj.active_material.name,end="")
        print('')
                
if weight != 0:
    center /= weight
    try:
        # move the CG marker object if present
        marker = bpy.data.objects[CGMarkerName]
        marker.location = center
    except KeyError:
        # or the cursor if no marker was found
        bpy.context.scene.cursor.location = center
    print (f"CG [{center.x:.2f} {center.y:.2f} {center.z:.2f}], total weight {weight:.2f}, done in {time.time()-start:.2f}s")
else :
    print ("No valid massive object found")
# throwing an exception is ugly, but a sure way to get the user's attention
if failure:
    msg = "Problems were detected. The result might be invalid"
    if Quiet: print(msg)
    else: raise Exception(msg)
$\endgroup$
2
  • $\begingroup$ Coming back to this after several years, I would be interested in reproducing this effect. The script as is gives me an error now (Blender 3.0). But maybe there is something new that has been implemented since then that could do the trick? bpy.ops.object.origin_set works for single objects. If there was a way to group objects and have a common center of origin, it would be ideal. $\endgroup$
    – Eli
    Jan 5 at 11:04
  • $\begingroup$ @Eli Here you go. A brand new version working with Blender 3.4.1. Probably just art for art's sake, but who knows? In case you're interested in this little hack and run into trouble, give me a holler and I'll see what I can do. $\endgroup$
    – kuroi neko
    Jan 12 at 0:40
2
$\begingroup$

In Blender 2.79 you can move the origin of an object to the center of mass, with the choice of computing it from the surface or the volume :

bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME')
bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')

"ORIGIN_CENTER_OF_MASS": Sets object origin to Center of Mass (Surface), Calculate the center of mass from the surface area.

"ORIGIN_CENTER_OF_VOLUME": Sets object origin to Center of Mass (Volume), Calculate the center of mass from the volume (must be manifold geometry with consistent normals).

See the docs here

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.