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