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"]/10] * 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)
118 bpy.ops.object.editmode_toggle()
119 bpy.data.curves[bpy.context.active_object.name].size = node["weight"] /10
120 bpy.data.curves[bpy.context.active_object.name].bevel_depth = 0.044
121 bpy.data.curves[bpy.context.active_object.name].offset = 0
122 bpy.data.curves[bpy.context.active_object.name].extrude = 0.2
123 bpy.data.curves[bpy.context.active_object.name].align = "CENTER"
124 bpy.context.active_object.rotation_euler = [1.5708,0,1.5708]
125 bpy.context.active_object.active_material = bpy.data.materials[col]
126 const = bpy.context.active_object.constraints.new(type='TRACK_TO')
127 const.target = bpy.data.objects['Camera']
128 const.track_axis = "TRACK_Z"
129 const.up_axis = "UP_Y"
131 #bpy.context.scene.objects.link(bpy.context.active_object)
132 #shapes.append(bpy.context.active_object)
133 #sha* 2 + [mag - node_size]
134 shapes_to_smooth.append(bpy.context.active_object)
137 for edge in network["edges"]:
139 # Get source and target locations by drilling down into data structure
140 source_loc = network["nodes"][edge["source"]]["location"]
141 source_loc = [val/divval for val in source_loc]
142 target_loc = network["nodes"][edge["target"]]["location"]
143 target_loc = [val / divval for val in target_loc]
145 diff = [c2 - c1 for c2, c1 in zip(source_loc, target_loc)]
146 cent = [(c2 + c1) / 2 for c2, c1 in zip(source_loc, target_loc)]
147 mag = sum([(c2 - c1) ** 2
148 for c1, c2 in zip(source_loc, target_loc)]) ** 0.5
150 # Euler rotation calculation
151 v_axis = Vector(diff).normalized()
152 v_obj = Vector((0, 0, 1))
153 v_rot = v_obj.cross(v_axis)
154 angle = acos(v_obj.dot(v_axis))
156 # Copy mesh primitive to create edge
157 edge_cylinder = cylinder.copy()
158 edge_cylinder.data = cylinder.data.copy()
159 edge_cylinder.dimensions = [float(edge['weight'])*10] * 2 + [mag - node_size]
160 #edge_cylinder.dimensions = [edge_thickness] * 2 + [mag - node_size]
161 edge_cylinder.location = cent
162 edge_cylinder.rotation_mode = "AXIS_ANGLE"
163 edge_cylinder.rotation_axis_angle = [angle] + list(v_rot)
164 bpy.context.scene.objects.link(edge_cylinder)
165 shapes.append(edge_cylinder)
166 shapes_to_smooth.append(edge_cylinder)
168 # Copy another mesh primitive to make an arrow head
170 arrow_cone = cone.copy()
171 arrow_cone.data = cone.data.copy()
172 arrow_cone.dimensions = [edge_thickness * 4.0] * 3
173 arrow_cone.location = cent
174 arrow_cone.rotation_mode = "AXIS_ANGLE"
175 arrow_cone.rotation_axis_angle = [angle + pi] + list(v_rot)
176 bpy.context.scene.objects.link(arrow_cone)
177 shapes.append(arrow_cone)
179 # Remove primitive meshes
180 bpy.ops.object.select_all(action='DESELECT')
182 cylinder.select = True
186 # If the starting cube is there, remove it
187 if "Cube" in bpy.data.objects.keys():
188 bpy.data.objects.get("Cube").select = True
189 bpy.ops.object.delete()
191 # Smooth specified shapes
192 for shape in shapes_to_smooth:
194 #bpy.context.scene.objects.active = shapes_to_smooth[0]
195 #bpy.ops.object.shade_smooth()
200 #bpy.context.scene.objects.active = shapes[0]
201 #bpy.ops.object.join()
203 # Center object origin to geometry
204 bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
207 bpy.context.scene.update()
209 # If main, load json and run
210 if __name__ == "__main__":
211 with open(sys.argv[3]) as network_file:
212 network = json.load(network_file)
213 draw_network(network)