3
$\begingroup$

I'm trying to create a curve with python to depict a bond between two atoms (spheres) like so:

bpy.ops.curve.primitive_bezier_curve_add()
obj = bpy.context.object
obj.data.dimensions = '3D'
obj.data.fill_mode = 'FULL'
obj.data.bevel_depth = self.bevel_depth
obj.data.bevel_resolution = self.bevel_resolution
# set first point to centre of atom_1
obj.data.splines[0].bezier_points[0].co = self.atom_1.locations[0]
obj.data.splines[0].bezier_points[0].handle_left_type = 'VECTOR'
# set second point to centre of atom_2
obj.data.splines[0].bezier_points[1].co = self.atom_2.locations[0]
obj.data.splines[0].bezier_points[1].handle_left_type = 'VECTOR'

The curve endpoints seem to get set to local coordinates and not to the centers of the spheres.

How do I add the hooks to attach the endpoints to the two spheres so that the curve ends stay attached to the spheres when animating the movement of the spheres?

$\endgroup$

5 Answers 5

6
$\begingroup$

(take a look at iwHiteRabbiT's answer if you want to avoid bpy.ops, which is always a good idea .) Based on the answer linked by Seppo Silaste I have come up with the following example code which you can modify to fit your application:

import bpy

def hookCurve(o1, o2, scn):
    curve = bpy.data.curves.new("link", 'CURVE')
    spline = curve.splines.new('BEZIER')

    spline.bezier_points.add(1)
    p0 = spline.bezier_points[0]
    p1 = spline.bezier_points[1]
    p0.co = o1.location
    p0.handle_right_type = 'VECTOR'
    p1.co = o2.location
    p1.handle_left_type = 'VECTOR'
    
    obj = bpy.data.objects.new("link", curve)
    
    m0 = obj.modifiers.new("alpha", 'HOOK')
    m0.object = o1
    m1 = obj.modifiers.new("beta", 'HOOK')
    m1.object = o2
    
    scn.objects.link(obj)
    scn.objects.active = obj
    
    # using anything in bpy.ops is a giant pain in the butt
    bpy.ops.object.mode_set(mode='EDIT')
    
    # the mode_set() invalidated the pointers, so get fresh ones
    p0 = curve.splines[0].bezier_points[0]
    p1 = curve.splines[0].bezier_points[1]
    
    p0.select_control_point=True
    bpy.ops.object.hook_assign(modifier="alpha")
    
    p0.select_control_point = False
    p1.select_control_point = True
    bpy.ops.object.hook_assign(modifier="beta")
    
    return obj

o1 = bpy.data.objects['atom 1']
o2 = bpy.data.objects['atom 2']

obj = hookCurve(o1,o2, bpy.context.scene)
$\endgroup$
5
  • $\begingroup$ This works! I'd up vote you but I don't have reputation ;o) I'd like to understand: Why does p0 and p1 have to be assigned again? Why create a curve and also an object with the curve? Thank you!! $\endgroup$
    – Melodius
    Jun 26, 2014 at 8:49
  • $\begingroup$ I moved them above the mode_set() call and the hook_assign() did not accomplish its mission. I'm assuming that the mode_set() changes the curve object in a way that invalidates the pointers inside the python objects p0 and p1. This is a common danger with blender's python API (and any other system that uses pointers, which is a lot of them). $\endgroup$
    – Mutant Bob
    Jun 26, 2014 at 14:10
  • $\begingroup$ Oh, and don't worry about upvoting. If this answer is actually correct and solves your problem, click the "accept answer" icon. If it does not, let me know how it fails so I can fix it. $\endgroup$
    – Mutant Bob
    Jun 26, 2014 at 14:33
  • $\begingroup$ 'bpy.context.object.modifiers["alpha"].subtarget = "bone_name"' this is needed if you are adding to a bone instead of an object. $\endgroup$ Mar 11 at 22:52
  • 1
    $\begingroup$ blender.stackexchange.com/questions/184565/… This shows a solution to add the hooks with out bpy.ops $\endgroup$ Mar 20 at 22:54
3
$\begingroup$

The accepted answer is indeed working but like said the author "using anything in bpy.ops is a giant pain in the butt".

So here's another solution where you won't have this pain in the butt :D

def hook_from_objetcs(objs, collections):
    name = '_'.join(['bezier'] + [o.name for o in objs])

    curve = bpy.data.curves.new('curve_' + name, 'CURVE')
    spline = curve.splines.new('BEZIER')
    obj = bpy.data.objects.new(name, curve)

    for col in collections:
        col.objects.link(obj)

    spline.bezier_points.add(len(objs) - 1)

    for i, o in enumerate(objs):
        p = spline.bezier_points[i]
        p.co = o.location
        p.handle_right_type = 'AUTO'
        p.handle_left_type = 'AUTO'

        h = obj.modifiers.new(o.name, 'HOOK')
        h.object = o
        h.vertex_indices_set([a + i*3 for a in range(3)])
$\endgroup$
1
  • $\begingroup$ I wish I had scrolled down. lol I came to the same conclusion after a lot of head banging. this is a better answer $\endgroup$ Mar 20 at 22:55
0
$\begingroup$

This is not a direct answer to the question, but rather to the underlying application of the OP.

I occasionally want to display proteins, where the hook method would be terribly slow (calling bpy.ops methods for >10000 bonds...). My current working solution for this specific problem of displaying a bond between two atoms is using constraints, which are much faster to handle with a script.

Here is a (rather lengthy) working example. As a bonus it reuses the data (mesh or beveled curve) for all bonds automatically. So the slow bpy.ops calls only happen once. They could be replaced entirely by creating the base mesh or curve from scratch but are more convenient in this case.

import bpy
import bmesh

def get_bond_data(type='MESH'):
    new_bond = None
    if type == 'MESH':
        data_name = "bond_mesh"
    elif type == 'CURVE':
        data_name = "bond_curve"

    data = bpy.context.blend_data.meshes.get(data_name)
    if not data:
        # save last selection to restore later
        selected = bpy.context.selected_objects
        last_active = bpy.context.object
        # bond should start at origin and have length 1 in y-direction
        if type == 'MESH':
            bpy.ops.mesh.primitive_cylinder_add(location=(0,0,0), 
                depth=1, radius=1.0,
                end_fill_type="NOTHING")
            new_bond = bpy.context.object
            data = new_bond.data
            data.name = data_name
            # rotate vertices 90 degrees around x, and shift along y axis
            bm = bmesh.new()
            bm.from_mesh(data)
            for vert in bm.verts:
                tmp_co = vert.co.copy()
                vert.co.y = -tmp_co.z + .5
                vert.co.z = tmp_co.y
            bm.to_mesh(data)
            bm.free()
            data.update()

        elif type == 'CURVE':
            bpy.ops.curve.primitive_bezier_curve_add(location=(0,0,0))
            new_bond = bpy.context.object
            data = new_bond.data
            data.name = data_name

            bp = data.splines[0].bezier_points
            for i in range(2):
                bp[i].co = (0,i,0)
                bp[i].handle_left_type = 'VECTOR'
                bp[i].handle_right_type = 'VECTOR'

            # get bevel object
            bevel_name = 'bond_bevel'
            bond_bevel = bpy.context.blend_data.objects.get(bevel_name)
            if not bond_bevel:
                bpy.ops.curve.primitive_bezier_circle_add(radius=1.0, location=(0,0,0))
                bond_bevel = bpy.context.object
                bond_bevel.name = bevel_name
            data.bevel_object = bond_bevel

    if new_bond:
        # finally delete object and reselect old selection
        bpy.context.scene.objects.unlink(new_bond)
        for o in selected:
            o.select = True
        bpy.context.scene.objects.active = last_active

    return data

def add_bond(first_atom, second_atom, type='MESH'):
    bond_name = "bond_{}-{}".format(first_atom.name, second_atom.name)
    bond_data = get_bond_data(type=type)
    new_bond = bpy.data.objects.new(bond_name, bond_data)
    bpy.context.scene.objects.link(new_bond)

    c = new_bond.constraints.new('COPY_LOCATION')
    c.name = "from"
    c.target = first_atom

    c = new_bond.constraints.new('STRETCH_TO')
    c.name = "to"
    c.rest_length = 1.0
    c.volume = 'NO_VOLUME'
    c.target = second_atom

    # scale bond so it looks better
    scale = 0.15
    new_bond.scale = (scale, 1.0, scale)

a1 = bpy.context.blend_data.objects.get("atom 1")
a2 = bpy.context.blend_data.objects.get("atom 2")
add_bond(a1, a2, type="CURVE")
#add_bond(a1, a2, type="MESH")
$\endgroup$
2
  • $\begingroup$ Hello what version of Blender is this written in? I'm getting an add_bond error: bond_name = "bond_{}-{}".format(first_atom.name, second_atom.name) AttributeError: 'NoneType' object has no attribute 'name' $\endgroup$
    – volvox
    Dec 10, 2015 at 8:50
  • $\begingroup$ @volvox I think it was 2.71 at the time, but it should work in other versions as well. You need to make sure that you have at least to objects in your scene called "atom 1" and "atom 2". $\endgroup$
    – FBA
    Dec 11, 2015 at 9:57
-1
$\begingroup$

As best I can tell blender does not have what you need for Bezier curves.

It does have something for meshes. There is a hook modifier you can apply to a mesh. You then specify a foreign object and a Vertex Group that will be hooked to that object.

So replace your Bezier curve with a simple mesh rectangle (or tube or whatever) and add the hook modifiers.

If you need python code to build the mesh, vertex groups, and modifiers reply in the comments and I'll cobble together something concrete.

$\endgroup$
5
  • 1
    $\begingroup$ Someone seems to think it is possible:blender.stackexchange.com/questions/13452/… $\endgroup$
    – Melodius
    Jun 25, 2014 at 16:53
  • $\begingroup$ I guess a cylinder could also work, but it doesn't have end points that I could set to the centers of the spheres. That's why I'm thinking about a curve. $\endgroup$
    – Melodius
    Jun 25, 2014 at 16:55
  • $\begingroup$ Put the ends of the cylinder such that the center of each cap is in the middle of a sphere. Make each cap its own vertex group for the Hook modifier. $\endgroup$
    – Mutant Bob
    Jun 25, 2014 at 17:27
  • $\begingroup$ Interesting, Seppo. I was discouraged because I added the modifier through the menu, not through Ctrl-H. There is no obvious readout in the modifier pane that shows you what curve control points are members of the hook, but it works. $\endgroup$
    – Mutant Bob
    Jun 25, 2014 at 19:12
  • $\begingroup$ The python API for the hook modifier object doesn't have any obvious ways to set which points on the curve are hooked. It looks like the only way to accomplish this is bpy.ops.object.hook_assign() which has all the drawbacks of everything in bpy.ops. But it is probably possible. $\endgroup$
    – Mutant Bob
    Jun 25, 2014 at 19:31
-1
$\begingroup$

If you want a cylinder to connect two objects a hook modifier probably isn't enough. As the atoms move relative to each other, the caps of the cylinder won't know to rotate to face the other object.

In order to address this shortcoming you can create a cylinder with a 2-bone armature. The cylinder has two vertex groups (one for each cap) to map them to each bone.

The bones then have bone constraints. The first bone constraint is a Copy Location to place the bone in the center of the atom. The second bone constraint is a Track To to point the bone at the other atom.

In order for this to achieve the desired effect the bones must be oriented correctly in edit mode. Notice how the top bone points down and has a roll of 180.

bones in edit mode.  Notice the roll on the top bone bones in pose mode.  Notice the order of the modifiers

If you need help figuring out the python to construct this sort of arrangement automatically, post a comment and I'll help figure it out.

$\endgroup$

You must log in to answer this question.

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