...
[iramuteq] / network_to_blender.py
1 """
2 Blender script. Draws a node-and-edge network in blender, randomly distributed
3 spherically.
4
5 14 Sept 2011: Added collision detection between nodes
6
7 30 Nov 2012: Rewrote. Switched to JSON, and large Blender speed boosts.
8
9 Written by Patrick Fuller, patrickfuller@gmail.com, 11 Sept 11
10
11 modifications by Pierre Ratinaud Feb 2014
12 """
13 import bpy
14 from math import acos, degrees, pi
15 from mathutils import Vector
16 from copy import copy
17
18 import json
19 from random import choice
20 import sys
21
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)}
27
28
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
36      
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
42      
43         # Transparency parameters
44         else :
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
56
57
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 """
60
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)})
65     make_colors(colors)
66     
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     
80
81     # Keep references to all nodes and edges
82     shapes = []
83     # Keep separate references to shapes to be smoothed
84     shapes_to_smooth = []
85     #val to div coordonnate
86     divval = 0.05
87
88     # Draw nodes
89     for key, node in network["nodes"].items():
90
91         # Coloring rule for nodes. Edit this to suit your needs!
92         col = str(tuple(node.get("color", choice(list(colors.keys())))))
93
94         # Copy mesh primitive and edit to make node
95         # (You can change the shape of drawn nodes here)
96         if spheres :
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]
103             #newmat.alpha = 0.01
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)
108         
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')
126
127         #Extrude the text
128         #bpy.context.object.data.extrude = 0.03
129
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"
136         
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)        
141
142     # Draw edges
143     for edge in network["edges"]:
144
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]
150
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
155
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))
161
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)
173
174         # Copy another mesh primitive to make an arrow head
175         if directed:
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)
184
185     # Remove primitive meshes
186     bpy.ops.object.select_all(action='DESELECT')
187     sphere.select = True
188     cylinder.select = True
189     cone.select = True
190     #text.select = True
191
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()
196
197     # Smooth specified shapes
198     for shape in shapes_to_smooth:
199         shape.select = True
200     #bpy.context.scene.objects.active = shapes_to_smooth[0]
201     #bpy.ops.object.shade_smooth()
202
203     # Join shapes
204     for shape in shapes:
205         shape.select = True
206     #bpy.context.scene.objects.active = shapes[0]
207     #bpy.ops.object.join()
208
209     # Center object origin to geometry
210     bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
211
212     # Refresh scene
213     bpy.context.scene.update()
214
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)