6
$\begingroup$

I'm trying to illustrate a 3D mathematical mesh of points with balls and rods (see screenshot) using a script. However, if I move a ball, the connecting rods would have to be updated because they are not connected.

Each ball object in the 3D array can be addressed by ob = La[iz][iy][ix] and each pair is connected by a call to connect_them(ob1, ob2, rrod) as shown in the script.

Is there some way I could adjust connect_them to make the connection permanent? So if I move one of the balls by 3D cursor or python, all of the rods that connect to it are carried along?

note: by "adjust connect_them" I mean implement this as a modification to the script - not do it by hand each time.

Let's assume that the radius of the balls is larger than the radius of the rods, and balls are not transparent, so any ugliness at the rod ends is inside the ball and therefore not visible.

note: I have maintained the directionality of the connector.

3d grid

def get_ico_data(n_subdiv=3, size=1.0, location=(0,0,1.0)):
    make_ico = bpy.ops.mesh.primitive_ico_sphere_add
    make_ico(subdivisions=n_subdiv, size=size, location=location)
    ico = bpy.context.active_object
    ico_data = ico.data.copy()
    bpy.ops.object.delete()
    return ico_data

def get_cyl_data(n_sides=12, radius=1.0, height=2.0, location=(0, 0, 1)):
    make_cyl = bpy.ops.mesh.primitive_cylinder_add
    make_cyl(vertices=n_sides, radius=radius, depth=height, location=location)
    cyl = bpy.context.active_object
    cyl_data = cyl.data.copy()
    bpy.ops.object.delete()
    return cyl_data

def make_new_meshobject_from_data(data, name=None, scene=None):
    if scene == None:
        scene = bpy.context.scene
    if name == None:
        name = "thing"
    me = bpy.data.meshes.new(name)
    ob = bpy.data.objects.new(name, me)
    ob.data = data.copy()
    scene.objects.link(ob)
    ob.select = True
    return ob

def connect_them(ob1, ob2, rad):
    loc1, loc2 = ob1.location, ob2.location
    vec = loc2 - loc1
    cen = 0.5*(loc1 + loc2)
    phi = np.arctan2(vec.y, vec.x)
    theta = np.arccos(vec.normalized().z)
    h = vec.length
    cyl = make_new_meshobject_from_data(cyl_data)
    cyl.location = cen
    cyl.scale = (rad, rad, h)
    cyl.rotation_euler = (0.0, theta, phi)
    return cyl

import bpy
import numpy as np

ico_data = get_ico_data(n_subdiv=2, size=1.0,               location=(0,0,0))
cyl_data = get_cyl_data(n_sides=12, radius=1.0, height=1.0, location=(0,0,0))

nx, ny, nz = 3, 5, 7

xa = np.arange(nx, dtype = 'float') - 1.0*float(nx-1)
ya = np.arange(ny, dtype = 'float') - 0.5*float(ny-1)
za = np.arange(nz, dtype = 'float') + 0.5

rsph = 0.12
La = []
for iz, z in enumerate(za):
    L1a = []
    for iy, y in enumerate(ya):
        L2a = []
        for ix, x in enumerate(xa):
            ico = make_new_meshobject_from_data(ico_data)
            ico.scale = [rsph]*3
            ico.location = [x, 2.0*y / np.sqrt(z+1.), 1.2*z**0.8] # uneven spacing illustration
            L2a.append(ico)
        L1a.append(L2a)
    La.append(L1a)

rrod = 0.04

# connect in z
for iz in range(nz-1):
    for iy in range(ny):
        for ix in range(nx):
            ob1 = La[iz][iy][ix]
            ob2 = La[iz+1][iy][ix]
            connect_them(ob1, ob2, rrod)

# connect in y
for iz in range(nz):
    for iy in range(ny-1):
        for ix in range(nx):
            ob1 = La[iz][iy][ix]
            ob2 = La[iz][iy+1][ix]
            connect_them(ob1, ob2, rrod)

# connect in x
for iz in range(nz):
    for iy in range(ny):
        for ix in range(nx-1):
            ob1 = La[iz][iy][ix]
            ob2 = La[iz][iy][ix+1]
            connect_them(ob1, ob2, rrod)

EDIT: So based on the suggestions in the answer by @TLousky I have this so far: with partial results. See new script below. I'm still trying to figure out how to script the "hair"!

3D griid hairy wireframe 1 3D griid hairy wireframe 2

def get_ico_data(n_subdiv=3, size=1.0, location=(0,0,1.0)):
    make_ico = bpy.ops.mesh.primitive_ico_sphere_add
    make_ico(subdivisions=n_subdiv, size=size, location=location)
    ico = bpy.context.active_object
    ico_data = ico.data.copy()
    bpy.ops.object.delete()
    return ico_data

def get_cyl_data(n_sides=12, radius=1.0, height=2.0, location=(0, 0, 1)):
    make_cyl = bpy.ops.mesh.primitive_cylinder_add
    make_cyl(vertices=n_sides, radius=radius, depth=height, location=location)
    cyl = bpy.context.active_object
    cyl_data = cyl.data.copy()
    bpy.ops.object.delete()
    return cyl_data

def make_new_meshobject_from_data(data, name=None, scene=None):
    if scene == None:
        scene = bpy.context.scene
    if name == None:
        name = "thing"
    me = bpy.data.meshes.new(name)
    ob = bpy.data.objects.new(name, me)
    ob.data = data.copy()
    scene.objects.link(ob)
    ob.select = True
    return ob

def connect_them(ob1, ob2, rad):
    loc1, loc2 = ob1.location, ob2.location
    vec = loc2 - loc1
    cen = 0.5*(loc1 + loc2)
    phi = np.arctan2(vec.y, vec.x)
    theta = np.arccos(vec.normalized().z)
    h = vec.length
    cyl = make_new_meshobject_from_data(cyl_data)
    cyl.location = cen
    cyl.scale = (rad, rad, h)
    cyl.rotation_euler = (0.0, theta, phi)
    return cyl

import bpy
import bmesh
import numpy as np

#ico_data = get_ico_data(n_subdiv=2, size=1.0,               location=(0,0,0))
#cyl_data = get_cyl_data(n_sides=12, radius=1.0, height=1.0, location=(0,0,0))

nx, ny, nz = 3, 5, 7

xa = np.arange(nx, dtype = 'float') - 1.0*float(nx-1)
ya = np.arange(ny, dtype = 'float') - 0.5*float(ny-1)
za = np.arange(nz, dtype = 'float') + 0.5

Xa, Ya, Za = np.meshgrid(xa, ya, za, indexing='ij')
# ico.location = [x, 2.0*y / np.sqrt(z+1.), 1.2*z**0.8]
Ya = 2.0*Ya / np.sqrt(Za+1.0)
Za = 1.2*Za**0.8

verts = list(zip(Xa.flatten(), Ya.flatten(), Za.flatten()))
nverts = nx*ny*nz

edges = []

# for z-edges
for i in range(nverts):
    if (i+1)%nz:  # all except for the last one in each a-row
        edges.append((i, i+1))

# for y-edges
for i in range(nverts):
    if ((i//nz)+1) % ny: # all except the last one in each y-row
        edges.append((i, i+nz))

# for x-edges
for i in range(nverts):
    if ((i//(nz*ny)) + 1) % nx:
        edges.append((i, i+nz*ny))

me = bpy.data.meshes.new("wire")
me.from_pydata(verts, edges, [])

obme = bpy.data.objects.new("wire_we_here", me)
obme.location = (0, 0, 0)  # bpy.context.scene.cursor_location

bpy.context.scene.objects.link(obme)
bpy.context.scene.objects.active = obme
obme.select=True

oo = bpy.ops.object
oo.modifier_add(type='HOOK')

oo.mode_set( mode = 'EDIT' )
bm = bmesh.from_edit_mesh( obme.data )

n = len( bm.verts )

for i in range(n):
    oo.mode_set( mode = 'EDIT' )
    bm = bmesh.from_edit_mesh( obme.data )

    bpy.ops.mesh.select_all( action = 'DESELECT' )
    bm.verts.ensure_lookup_table()
    bm.verts[i].select = True
    bm.select_flush( True )

    bpy.ops.object.hook_add_newob()

    bpy.ops.object.mode_set( mode = 'OBJECT' )
    bpy.ops.object.select_all( action = 'DESELECT' )

    obme.select = True
    bpy.context.scene.objects.active = obme

do_the_rest_of_it = True
if do_the_rest_of_it:

    ico_data = get_ico_data(n_subdiv=3, size=0.5, location=(0,0,0.0))

    sphere_obj = make_new_meshobject_from_data(ico_data, name="ball")

    #sphere_obj = bpy.context.scene.objects['Sphere']

    empties = [ o for o in bpy.context.scene.objects if o.type == 'EMPTY' ]
    for e in empties:
        e.empty_draw_type = 'SPHERE'
        #e.empty_draw_size = 0.2
        e.empty_draw_size = sphere_obj.dimensions.z / 5.0

    obme.select = True
    bpy.context.scene.objects.active = obme

    oo = bpy.ops.object
    oo.modifier_add(type='PARTICLE_SYSTEM')
    oo.modifier_add(type='SKIN')

It Works! The solution by @TLousky in the answer works great!

enter image description here

$\endgroup$
  • $\begingroup$ The last image (8x10x12 grid with glowing blue inside) was made using the excellent answer of @TLousky below. $\endgroup$ – uhoh Sep 22 '15 at 4:16
  • $\begingroup$ balls and rods made from skin and hair (modifiers and particles) - creepy :) $\endgroup$ – uhoh Sep 22 '15 at 6:47
5
$\begingroup$

I'd like to suggest a different approach to solve this (download blend file here), that might be simpler.

The mesh is a simple edge wireframe, and the spheres are generated by a particle system (type hair, count = number of verts, render type = object, emit from verts without any randomness and set object size to 0.1).

The wireframe is displayed using a skin modifier with a subdivision surface.

Illustration

And to move every sphere and the mesh connected to it, you can use a hook per vertex, which can be rather easily created with this script:

import bpy, bmesh

o = bpy.context.object

bpy.ops.object.mode_set( mode = 'EDIT' )
bm = bmesh.from_edit_mesh( o.data )

n = len( bm.verts )

for i in range(n):
    bpy.ops.object.mode_set( mode = 'EDIT' )
    bm = bmesh.from_edit_mesh( o.data )

    bpy.ops.mesh.select_all( action = 'DESELECT' )
    bm.verts.ensure_lookup_table()
    bm.verts[i].select = True
    bm.select_flush( True )

    bpy.ops.object.hook_add_newob()

    bpy.ops.object.mode_set( mode = 'OBJECT' )
    bpy.ops.object.select_all( action = 'DESELECT' )

    o.select = True
    bpy.context.scene.objects.active = o

If you want it to look nicer you can set the hook empties to be of type sphere and adjust their size to fit perfectly to the sphere object you use to create the balls on each vert:

sphereObj = bpy.context.scene.objects['Sphere']
empties = [ o for o in C.scene.objects if o.type == 'EMPTY' ]
for e in empties:
    e.empty_draw_type = 'SPHERE'
    e.empty_draw_size = sphereObj.dimensions.z / 5

Then all you have to do is make your mesh unselectable and manipulate it only with the hooks.

Final result

EDIT: I completed the script including creating the particle system, setting the skin radius and adding the modifiers and hooks, you just need to replace the arbitrary mesh data with the one you actually need:

import bpy, bmesh

# Sample mesh - a subdivided cube 
verts = [(-1.0, -1.0, -1.0), (-1.0, -1.0, 1.0), (-1.0, 1.0, -1.0), (-1.0, 1.0, 1.0), (1.0, -1.0, -1.0), (1.0, -1.0, 1.0), (1.0, 1.0, -1.0), (1.0, 1.0, 1.0), (0.0, 1.0, 1.0), (0.0, 1.0, -1.0), (0.0, -1.0, 1.0), (0.0, -1.0, -1.0), (-1.0, -1.0, 0.0), (-1.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 1.0), (-1.0, 0.0, -1.0), (1.0, 0.0, 1.0), (1.0, 0.0, -1.0), (0.0, 0.0, 1.0), (0.0, 0.0, -1.0), (1.0, 0.0, 0.0), (-1.0, 0.0, 0.0)]

edges = [(12, 1), (18, 3), (13, 2), (19, 0), (8, 7), (14, 6), (9, 2), (20, 5), (15, 4), (21, 6), (10, 1), (11, 4), (3, 8), (6, 9), (5, 10), (0, 11), (22, 10), (23, 11), (16, 11), (17, 9), (0, 12), (3, 13), (7, 14), (5, 15), (10, 16), (8, 17), (17, 13), (16, 15), (12, 16), (24, 14), (14, 17), (25, 12), (1, 18), (2, 19), (7, 20), (4, 21), (8, 22), (9, 23), (15, 24), (13, 25), (18, 25), (20, 24), (19, 23), (20, 22), (22, 18), (23, 21), (24, 21), (25, 19)]

## Generate Mesh from pydata
def make_mesh( context, name, verts, edges ):
    mesh = bpy.data.meshes.new( name = name )

    o = bpy.data.objects.new(name, mesh)
    o.location = (0,0,0) # place at object origin
    context.scene.objects.link( o )

    mesh.from_pydata( verts, edges, [] )

    return o

o = make_mesh( bpy.context, 'myMesh', verts, edges )

o.hide_select = True # Make mesh unclickable

## Create sphere for particle system
bpy.ops.mesh.primitive_ico_sphere_add( size = 0.5 )
s = bpy.context.scene.objects[ bpy.context.object.name ]
s.layers = [ False for i in range(19) ] + [ True ] # Place sphere on last layer only

## Add hooks
bpy.ops.object.select_all( action = 'DESELECT' )
o.select = True
bpy.context.scene.objects.active = o

bpy.ops.object.mode_set( mode = 'EDIT' )
bm = bmesh.from_edit_mesh( o.data )

n = len( bm.verts )

for i in range(n):
    bpy.ops.object.mode_set( mode = 'EDIT' )
    bm = bmesh.from_edit_mesh( o.data )

    bpy.ops.mesh.select_all( action = 'DESELECT' )
    bm.verts.ensure_lookup_table()
    bm.verts[i].select = True
    bm.select_flush( True )

    bpy.ops.object.hook_add_newob()

    bpy.ops.object.mode_set( mode = 'OBJECT' )
    bpy.ops.object.select_all( action = 'DESELECT' )

    o.select = True
    bpy.context.scene.objects.active = o

empties = [ o for o in bpy.context.scene.objects if o.type == 'EMPTY' ]
for e in empties:
    e.empty_draw_type = 'SPHERE'
    e.empty_draw_size = s.dimensions.z / 5

## Add and set particle system
bpy.ops.object.particle_system_add()

psys = o.particle_systems[0].settings
psys.count             = len( o.data.vertices )
psys.type              = 'HAIR'
psys.use_advanced_hair = True
psys.emit_from         = 'VERT'
psys.use_emit_random   = False
psys.render_type       = 'OBJECT'
psys.dupli_object      = s
psys.particle_size     = 0.1

## Set up skin and subsurf modifiers
bpy.ops.object.modifier_add( type = 'SKIN' )

# Set skin radius for all verts 
for v in o.data.skin_vertices[0].data:
    v.radius = [ 0.05, 0.05 ]

bpy.ops.object.modifier_add( type = 'SUBSURF' )
$\endgroup$
  • $\begingroup$ Wow a hairy wireframe! OK I will get back to a computer in a bit and take it for a spin in the scripting window. So I think that you are thinking that I should create the 3D wireframe topology with newmesh.from_pydata()? $\endgroup$ – uhoh Sep 21 '15 at 14:29
  • $\begingroup$ Yup, just verts and edges. Hairy wireframe :-D $\endgroup$ – TLousky Sep 21 '15 at 14:36
  • $\begingroup$ I think I'm close now!! - see edit to question. This thing needs to create itself, so I need to do all steps within the script. $\endgroup$ – uhoh Sep 21 '15 at 16:32
  • $\begingroup$ @uhoh - I completed the script (see the one at the bottom), just replace the arbitrary mesh data with your own and it should work. Run it in an empty scene to test and see exactly what's going on. $\endgroup$ – TLousky Sep 21 '15 at 17:34
  • 1
    $\begingroup$ +1 Fantastic! This works great!! I've added one more image at the bottom of the question. So to make the mesh "wiggle and dance" just need to access the locations of the `Empty.xxx' objects. Thank you @TLousky! You've also taught a lot about particle system scripting as well. Super! $\endgroup$ – uhoh Sep 22 '15 at 4:13
3
$\begingroup$

Another option after creating a sphere at each point is to use a curve to join each pair together. A curve with two points and two hook modifiers can keep each end of the curve attached to the sphere and with vector handles they will stay straight between each end.

A nice bonus is being able to add a driver to the bevel_depth that will give you an automatic size change based on the distance (like this answer), so it can get thinner as the two ends are stretched apart.

enter image description here

While the code to replace your connect_them() looks large it is just setting options, nothing fancy.

conRes = 3

def connect_them(ob1, ob2, rad):
    # create the curve data
    jd = bpy.data.curves.new(name='curve',type='CURVE')
    #jd.bevel_depth = rad
    jd.bevel_resolution = conRes
    jd.dimensions = '3D'
    jd.fill_mode = 'FULL'

    # create the curve object
    jo = bpy.data.objects.new('joiner', jd)
    bpy.context.scene.objects.link(jo)
    spl = jd.splines.new(type='BEZIER')
    spl.bezier_points.add()
    p1 = spl.bezier_points[0]
    p1.handle_left_type = 'VECTOR'
    p1.handle_right_type = 'VECTOR'
    p1.co = ob1.location
    p2 = spl.bezier_points[1]
    p2.handle_left_type = 'VECTOR'
    p2.handle_right_type = 'VECTOR'
    p2.co = ob2.location

    # create hook modifiers
    scene.objects.active = jo
    bpy.ops.object.mode_set(mode="EDIT")
    p = jd.splines[0].bezier_points
    p[0].select_control_point = True
    p[1].select_control_point = False
    hm1 = jo.modifiers.new(name='Hook1',type='HOOK')
    hm1.object = ob1
    bpy.ops.object.hook_assign(modifier='Hook1')

    p[0].select_control_point = False
    p[1].select_control_point = True
    hm2 = jo.modifiers.new(name='Hook2',type='HOOK')
    hm2.object = ob2
    bpy.ops.object.hook_assign(modifier='Hook2')

    bpy.ops.object.mode_set(mode="OBJECT")

    # add driver to alter diamater
    bev_drv = bpy.data.objects['joiner'].data.driver_add('bevel_depth')
    dv = bev_drv.driver.variables.new()
    dv.name = 'dist'
    dv.type = 'LOC_DIFF'
    dv.targets[0].id = ob1
    dv.targets[1].id = ob2
    bev_drv.driver.expression = '1.0-(dist*0.1)'
$\endgroup$
  • $\begingroup$ That's pretty cool! Thanks @sambler, I'm going to try it out in the next day or two but wanted to let you know it's very much appreciated. I may ask a follow up question specific to using curves/splines, and I'll leave a link to it here. These examples are really useful! $\endgroup$ – uhoh Sep 23 '15 at 15:30

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.