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.
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"!
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!






