labbe + chrono
[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.1
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"]/10] * 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         
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"
130         
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)        
135
136     # Draw edges
137     for edge in network["edges"]:
138
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]
144
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
149
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))
155
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)
167
168         # Copy another mesh primitive to make an arrow head
169         if directed:
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)
178
179     # Remove primitive meshes
180     bpy.ops.object.select_all(action='DESELECT')
181     sphere.select = True
182     cylinder.select = True
183     cone.select = True
184     #text.select = True
185
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()
190
191     # Smooth specified shapes
192     for shape in shapes_to_smooth:
193         shape.select = True
194     #bpy.context.scene.objects.active = shapes_to_smooth[0]
195     #bpy.ops.object.shade_smooth()
196
197     # Join shapes
198     for shape in shapes:
199         shape.select = True
200     #bpy.context.scene.objects.active = shapes[0]
201     #bpy.ops.object.join()
202
203     # Center object origin to geometry
204     bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
205
206     # Refresh scene
207     bpy.context.scene.update()
208
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)