Correct error in last commit
[blender.git] / release / datafiles / blender_icons_geom.py
1 # Apache License, Version 2.0
2
3 """
4 Example Usage
5 =============
6
7 Command line::
8
9    ./blender.bin \
10        icon_file.blend --background --python ./release/datafiles/blender_icons_geom.py -- \
11        --output-dir=./release/datafiles/blender_icons_geom
12
13 Icon Format
14 ===========
15
16 This is a simple binary format (all bytes, so no endian).
17
18 The header is 8 bytes:
19
20 :0..3: ``VCO``: identifier.
21 :4: ``0``: icon file version.
22 :5: icon size-x.
23 :6: icon size-y.
24 :7: icon start-x.
25 :8: icon start-y.
26
27 Icon width and height are for icons that don't use the full byte range
28 (so we don't get bad alignment for 48 pixel grid for eg).
29
30 Start values are currently unused.
31
32 After the header, the remaining length of the data defines the geometry size.
33
34 :6 bytes each: triangle (XY) locations.
35 :12 bytes each: triangle (RGBA) locations.
36
37 All coordinates are written, then all colors.
38
39 Since this is a binary format which isn't intended for general use
40 the ``.dat`` file extension should be used.
41 """
42
43 # This script writes out geometry-icons.
44 import bpy
45
46 # Generic functions
47
48 def area_tri_signed_2x_v2(v1, v2, v3):
49         return (v1[0] - v2[0]) * (v2[1] - v3[1]) + (v1[1] - v2[1]) * (v3[0] - v2[0])
50
51
52 class TriMesh:
53     """
54     Triangulate, may apply other changes here too.
55     """
56     __slots__ = ("object", "mesh")
57
58     def __init__(self, ob):
59         self.object = ob
60         self.mesh = None
61
62     def __enter__(self):
63         self.mesh = self._tri_copy_from_object(self.object)
64         return self.mesh
65
66     def __exit__(self, *args):
67         bpy.data.meshes.remove(self.mesh)
68
69     @staticmethod
70     def _tri_copy_from_object(ob):
71         import bmesh
72         assert(ob.type == 'MESH')
73         bm = bmesh.new()
74         bm.from_mesh(ob.data)
75         bmesh.ops.triangulate(bm, faces=bm.faces)
76         me = bpy.data.meshes.new(ob.name + ".copy")
77         bm.to_mesh(me)
78         bm.free()
79         return me
80
81
82 def object_material_colors(ob):
83     material_colors = []
84     color_default = (1.0, 1.0, 1.0, 1.0)
85     for slot in ob.material_slots:
86         material = slot.material
87         color = color_default
88         if material is not None and material.use_nodes:
89             node_tree = material.node_tree
90             if node_tree is not None:
91                 color = next((
92                     node.outputs[0].default_value[:]
93                     for node in node_tree.nodes
94                     if node.type == 'RGB'
95                 ), color_default)
96         if min(color) < 0.0 or max(color) > 1.0:
97             print(f"Material: {material.name!r} has color out of 0..1 range {color!r}")
98             color = tuple(max(min(c, 1.0), 0.0) for c in color)
99         material_colors.append(color)
100     return material_colors
101
102
103 def object_child_map(objects):
104     objects_children = {}
105     for ob in objects:
106         ob_parent = ob.parent
107         # Get the root.
108         if ob_parent is not None:
109             while ob_parent and ob_parent.parent:
110                 ob_parent = ob_parent.parent
111         if ob_parent is not None:
112             objects_children.setdefault(ob_parent, []).append(ob)
113     for ob_all in objects_children.values():
114         ob_all.sort(key=lambda ob: ob.name)
115     return objects_children
116
117
118 def mesh_data_lists_from_mesh(me, material_colors):
119     me_loops = me.loops[:]
120     me_loops_color = me.vertex_colors.active.data[:]
121     me_verts = me.vertices[:]
122     me_polys = me.polygons[:]
123
124     tris_data = []
125
126     for p in me_polys:
127         # Note, all faces are handled, backfacing/zero area is checked just before writing.
128         material_index = p.material_index
129         if material_index < len(material_colors):
130             base_color = material_colors[p.material_index]
131         else:
132             base_color = (1.0, 1.0, 1.0, 1.0)
133
134         l_sta = p.loop_start
135         l_len = p.loop_total
136         loops_poly = me_loops[l_sta:l_sta + l_len]
137         color_poly = me_loops_color[l_sta:l_sta + l_len]
138         i0 = 0
139         i1 = 1
140
141         # we only write tris now
142         assert(len(loops_poly) == 3)
143
144         for i2 in range(2, l_len):
145             l0 = loops_poly[i0]
146             l1 = loops_poly[i1]
147             l2 = loops_poly[i2]
148
149             c0 = color_poly[i0]
150             c1 = color_poly[i1]
151             c2 = color_poly[i2]
152
153             v0 = me_verts[l0.vertex_index]
154             v1 = me_verts[l1.vertex_index]
155             v2 = me_verts[l2.vertex_index]
156
157             tris_data.append((
158                 # float depth
159                 p.center.z,
160                 # XY coords.
161                 (
162                     v0.co.xy[:],
163                     v1.co.xy[:],
164                     v2.co.xy[:],
165                 ),
166                 # RGBA color.
167                 tuple((
168                     [int(c * b * 255) for c, b in zip(cn.color, base_color)]
169                     for cn in (c0, c1, c2)
170                 )),
171             ))
172             i1 = i2
173     return tris_data
174
175
176 def mesh_data_lists_from_objects(ob_parent, ob_children):
177     tris_data = []
178
179     has_parent = False
180     if ob_children:
181         parent_matrix = ob_parent.matrix_world.copy()
182         parent_matrix_inverted = parent_matrix.inverted()
183
184     for ob in (ob_parent, *ob_children):
185         with TriMesh(ob) as me:
186             if has_parent:
187                 me.transform(parent_matrix_inverted @ ob.matrix_world)
188
189             tris_data.extend(
190                 mesh_data_lists_from_mesh(
191                     me,
192                     object_material_colors(ob),
193                 )
194             )
195         has_parent = True
196     return tris_data
197
198
199 def write_mesh_to_py(fh, ob, ob_children):
200
201     def float_as_byte(f, axis_range):
202         assert(axis_range <= 255)
203         # -1..1 -> 0..255
204         f = (f + 1.0) * 0.5
205         f = int(round(f * axis_range))
206         return min(max(f, 0), axis_range)
207
208     def vert_as_byte_pair(v):
209         return (
210             float_as_byte(v[0], coords_range_align[0]),
211             float_as_byte(v[1], coords_range_align[1]),
212         )
213
214     tris_data = mesh_data_lists_from_objects(ob, ob_children)
215
216     # 100 levels of Z depth, round to avoid differences from precision error
217     # causing different computers to write triangles in more or less random order.
218     tris_data.sort(key=lambda data: int(data[0] * 100))
219
220     if 0:
221         # make as large as we can, keeping alignment
222         def size_scale_up(size):
223             assert(size != 0)
224             while size * 2 <= 255:
225                 size *= 2
226             return size
227
228         coords_range = (
229             size_scale_up(ob.get("size_x")) or 255,
230             size_scale_up(ob.get("size_y")) or 255,
231         )
232     else:
233         # disable for now
234         coords_range = 255, 255
235
236     # Pixel size needs to be increased since a pixel needs one extra geom coordinate,
237     # if we're writing 32 pixel, align verts to 33.
238     coords_range_align = tuple(min(c + 1, 255) for c in coords_range)
239
240     print("Writing:", fh.name, coords_range)
241
242     fw = fh.write
243
244     # Header (version 0).
245     fw(b'VCO\x00')
246     # Width, Height
247     fw(bytes(coords_range))
248     # X, Y
249     fw(bytes((0, 0)))
250
251     # Once converted into bytes, the triangle might become zero area
252     tri_skip = [False] * len(tris_data)
253     for i, (_, tri_coords, _) in enumerate(tris_data):
254         tri_coords_as_byte = [vert_as_byte_pair(vert) for vert in tri_coords]
255         if area_tri_signed_2x_v2(*tri_coords_as_byte) <= 0:
256             tri_skip[i] = True
257             continue
258         for vert_byte in tri_coords_as_byte:
259             fw(bytes(vert_byte))
260     for i, (_, _, tri_color) in enumerate(tris_data):
261         if tri_skip[i]:
262             continue
263         for color in tri_color:
264             fw(bytes(color))
265
266
267 def create_argparse():
268     import argparse
269     parser = argparse.ArgumentParser()
270     parser.add_argument(
271         "--output-dir",
272         dest="output_dir",
273         default=".",
274         type=str,
275         metavar="DIR",
276         required=False,
277         help="Directory to write icons to.",
278     )
279     parser.add_argument(
280         "--group",
281         dest="group",
282         default="",
283         type=str,
284         metavar="GROUP",
285         required=False,
286         help="Group name to export from (otherwise export all objects).",
287     )
288     return parser
289
290
291 def main():
292     import os
293     import sys
294     parser = create_argparse()
295     if "--" in sys.argv:
296         argv = sys.argv[sys.argv.index("--") + 1:]
297     else:
298         argv = []
299     args = parser.parse_args(argv)
300
301     objects = []
302
303     if args.group:
304         group = bpy.data.collections.get(args.group)
305         if group is None:
306             print(f"Group {args.group!r} not found!")
307             return
308         objects_source = group.objects
309         del group
310     else:
311         objects_source = bpy.data.objects
312
313     for ob in objects_source:
314
315         # Skip non-mesh objects
316         if ob.type != 'MESH':
317             continue
318         name = ob.name
319
320         # Skip copies of objects
321         if name.rpartition(".")[2].isdigit():
322             continue
323
324         if not ob.data.vertex_colors:
325             print("Skipping:", name, "(no vertex colors)")
326             continue
327
328         objects.append((name, ob))
329
330     objects.sort(key=lambda a: a[0])
331
332     objects_children = object_child_map(bpy.data.objects)
333
334     for name, ob in objects:
335         if ob.parent:
336             continue
337         filename = os.path.join(args.output_dir, name + ".dat")
338         with open(filename, 'wb') as fh:
339             write_mesh_to_py(fh, ob, objects_children.get(ob, []))
340
341
342 if __name__ == "__main__":
343     main()