2 Blender script. Draws a node-and-edge network in blender, randomly distributed
5 14 Sept 2011: Added collision detection between nodes
7 30 Nov 2012: Rewrote. Switched to JSON, and large Blender speed boosts.
9 Written by Patrick Fuller, patrickfuller@gmail.com, 11 Sept 11
11 modifications by Pierre Ratinaud Feb 2014
14 from math import acos, degrees, pi
15 from mathutils import Vector
19 from random import choice
22 # Colors to turn into materials
23 #colors = {"purple": (178, 132, 234), "gray": (11, 11, 11),
24 # "green": (114, 195, 0), "red": (255, 0, 75),
25 # "blue": (0, 131, 255), "clear": (0, 131, 255),
26 # "yellow": (255, 187, 0), "light_gray": (118, 118, 118)}
29 # Normalize to [0,1] and make blender materials
30 def make_colors(colors):
31 for key, value in colors.items():
32 value = [x / 255.0 for x in value]
33 bpy.data.materials.new(name=key)
34 bpy.data.materials[key].diffuse_color = value
35 bpy.data.materials[key].specular_intensity = 0.5
37 # Don't specify more parameters if these colors
38 if key == "gray" or key == "light_gray":
39 bpy.data.materials[key].use_transparency = True
40 bpy.data.materials[key].transparency_method = "Z_TRANSPARENCY"
41 bpy.data.materials[key].alpha = 0.2
43 # Transparency parameters
45 bpy.data.materials[key].use_transparency = True
46 bpy.data.materials[key].transparency_method = "Z_TRANSPARENCY"
47 bpy.data.materials[key].alpha = 0.6 if key == "clear" else 0.8
48 bpy.data.materials.new(name = key + 'sphere')
49 bpy.data.materials[key + 'sphere'].diffuse_color = value
50 bpy.data.materials[key + 'sphere'].specular_intensity = 0.1
51 bpy.data.materials[key + 'sphere'].use_transparency = True
52 bpy.data.materials[key + 'sphere'].transparency_method = "Z_TRANSPARENCY"
53 bpy.data.materials[key + 'sphere'].alpha = 0.1
54 #bpy.data.materials[key].raytrace_transparency.fresnel = 0.1
55 #bpy.data.materials[key].raytrace_transparency.ior = 1.15
58 def draw_network(network, edge_thickness=0.25, node_size=3, directed=False, spheres = True):
59 """ Takes assembled network/molecule data and draws to blender """
61 colors = [tuple(network["nodes"][node]['color']) for node in network["nodes"]]
62 cols = list(set(colors))
63 colors = dict(zip([str(col) for col in cols],cols))
64 colors.update({"light_gray": (118, 118, 118), "gray": (11, 11, 11)})
67 # Add some mesh primitives
68 bpy.ops.object.select_all(action='DESELECT')
69 #bpy.ops.mesh.primitive_uv_sphere_add()
70 bpy.ops.mesh.primitive_uv_sphere_add(segments = 64, ring_count = 32)
71 sphere = bpy.context.object
72 bpy.ops.mesh.primitive_cylinder_add()
73 cylinder = bpy.context.object
74 cylinder.active_material = bpy.data.materials["light_gray"]
75 bpy.ops.mesh.primitive_cone_add()
76 cone = bpy.context.object
77 cone.active_material = bpy.data.materials["light_gray"]
78 #bpy.ops.object.text_add(view_align=True)
81 # Keep references to all nodes and edges
83 # Keep separate references to shapes to be smoothed
85 #val to div coordonnate
89 for key, node in network["nodes"].items():
91 # Coloring rule for nodes. Edit this to suit your needs!
92 col = str(tuple(node.get("color", choice(list(colors.keys())))))
94 # Copy mesh primitive and edit to make node
95 # (You can change the shape of drawn nodes here)
97 node_sphere = sphere.copy()
98 node_sphere.data = sphere.data.copy()
99 node_sphere.location = [val/divval for val in node["location"]]
100 #node_sphere.dimensions = [node_size] * 3
101 node_sphere.dimensions = [node["weight"]] * 3
102 #newmat = bpy.data.materials[col]
104 node_sphere.active_material = bpy.data.materials[col + 'sphere']
105 bpy.context.scene.objects.link(node_sphere)
106 shapes.append(node_sphere)
107 shapes_to_smooth.append(node_sphere)
109 #node_text = text.copy()
110 #node_text.data = text.data.copy()
111 #node_text.location = node["location"]
112 bpy.ops.object.text_add(view_align=False, location = [val/divval for val in node["location"]])
113 #bpy.ops.object.text_add(view_align=False, location = [val for val in node["location"]])
114 bpy.ops.object.editmode_toggle()
115 bpy.ops.font.delete()
116 bpy.ops.font.text_insert(text=key)
117 bpy.ops.object.editmode_toggle()
118 bpy.data.curves[bpy.context.active_object.name].size = node["weight"]/2
119 bpy.data.curves[bpy.context.active_object.name].bevel_depth = 0.044
120 bpy.data.curves[bpy.context.active_object.name].offset = 0
121 bpy.data.curves[bpy.context.active_object.name].extrude = 0.2
122 bpy.data.curves[bpy.context.active_object.name].align = "CENTER"
123 bpy.context.active_object.rotation_euler = [1.5708,0,1.5708]
124 bpy.context.active_object.active_material = bpy.data.materials[col]
125 #bpy.ops.object.mode_set(mode='OBJECT')
128 #bpy.context.object.data.extrude = 0.03
130 #Convert text to mesh
131 #bpy.context.active_object.convert(target='MESH', keep_original=False)
132 const = bpy.context.active_object.constraints.new(type='TRACK_TO')
133 const.target = bpy.data.objects['Camera']
134 const.track_axis = "TRACK_Z"
135 const.up_axis = "UP_Y"
137 #bpy.context.scene.objects.link(bpy.context.active_object)
138 #shapes.append(bpy.context.active_object)
139 #sha* 2 + [mag - node_size]
140 shapes_to_smooth.append(bpy.context.active_object)
143 for edge in network["edges"]:
145 # Get source and target locations by drilling down into data structure
146 source_loc = network["nodes"][edge["source"]]["location"]
147 source_loc = [val/divval for val in source_loc]
148 target_loc = network["nodes"][edge["target"]]["location"]
149 target_loc = [val / divval for val in target_loc]
151 diff = [c2 - c1 for c2, c1 in zip(source_loc, target_loc)]
152 cent = [(c2 + c1) / 2 for c2, c1 in zip(source_loc, target_loc)]
153 mag = sum([(c2 - c1) ** 2
154 for c1, c2 in zip(source_loc, target_loc)]) ** 0.5
156 # Euler rotation calculation
157 v_axis = Vector(diff).normalized()
158 v_obj = Vector((0, 0, 1))
159 v_rot = v_obj.cross(v_axis)
160 angle = acos(v_obj.dot(v_axis))
162 # Copy mesh primitive to create edge
163 edge_cylinder = cylinder.copy()
164 edge_cylinder.data = cylinder.data.copy()
165 edge_cylinder.dimensions = [float(edge['weight'])/10] * 2 + [mag - node_size]
166 #edge_cylinder.dimensions = [edge_thickness] * 2 + [mag - node_size]
167 edge_cylinder.location = cent
168 edge_cylinder.rotation_mode = "AXIS_ANGLE"
169 edge_cylinder.rotation_axis_angle = [angle] + list(v_rot)
170 bpy.context.scene.objects.link(edge_cylinder)
171 shapes.append(edge_cylinder)
172 shapes_to_smooth.append(edge_cylinder)
174 # Copy another mesh primitive to make an arrow head
176 arrow_cone = cone.copy()
177 arrow_cone.data = cone.data.copy()
178 arrow_cone.dimensions = [edge_thickness * 4.0] * 3
179 arrow_cone.location = cent
180 arrow_cone.rotation_mode = "AXIS_ANGLE"
181 arrow_cone.rotation_axis_angle = [angle + pi] + list(v_rot)
182 bpy.context.scene.objects.link(arrow_cone)
183 shapes.append(arrow_cone)
185 # Remove primitive meshes
186 bpy.ops.object.select_all(action='DESELECT')
188 cylinder.select = True
192 # If the starting cube is there, remove it
193 if "Cube" in bpy.data.objects.keys():
194 bpy.data.objects.get("Cube").select = True
195 bpy.ops.object.delete()
197 # Smooth specified shapes
198 for shape in shapes_to_smooth:
200 #bpy.context.scene.objects.active = shapes_to_smooth[0]
201 #bpy.ops.object.shade_smooth()
206 #bpy.context.scene.objects.active = shapes[0]
207 #bpy.ops.object.join()
209 # Center object origin to geometry
210 bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
213 bpy.context.scene.update()
215 # If main, load json and run
216 if __name__ == "__main__":
217 with open(sys.argv[3]) as network_file:
218 network = json.load(network_file)
219 draw_network(network)