1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19
20if "bpy" in locals():
21    from importlib import reload
22
23    utils = reload(utils)
24else:
25    from blenderkit import utils
26
27import bpy
28from object_print3d_utils import operators as ops
29
30RENDER_OBTYPES = ['MESH', 'CURVE', 'SURFACE', 'METABALL', 'TEXT']
31
32
33def check_material(props, mat):
34    e = bpy.context.scene.render.engine
35    shaders = []
36    textures = []
37    props.texture_count = 0
38    props.node_count = 0
39    props.total_megapixels = 0
40    props.is_procedural = True
41
42    if e == 'CYCLES':
43
44        if mat.node_tree is not None:
45            checknodes = mat.node_tree.nodes[:]
46            while len(checknodes) > 0:
47                n = checknodes.pop()
48                props.node_count += 1
49                if n.type == 'GROUP':  # dive deeper here.
50                    checknodes.extend(n.node_tree.nodes)
51                if len(n.outputs) == 1 and n.outputs[0].type == 'SHADER' and n.type != 'GROUP':
52                    if n.type not in shaders:
53                        shaders.append(n.type)
54                if n.type == 'TEX_IMAGE':
55
56                    if n.image is not None:
57                        mattype = 'image based'
58                        props.is_procedural = False
59                        if n.image not in textures:
60                            textures.append(n.image)
61                            props.texture_count += 1
62                            props.total_megapixels += (n.image.size[0] * n.image.size[1])
63
64                            maxres = max(n.image.size[0], n.image.size[1])
65                            props.texture_resolution_max = max(props.texture_resolution_max, maxres)
66                            minres = min(n.image.size[0], n.image.size[1])
67                            if props.texture_resolution_min == 0:
68                                props.texture_resolution_min = minres
69                            else:
70                                props.texture_resolution_min = min(props.texture_resolution_min, minres)
71
72    props.shaders = ''
73    for s in shaders:
74        if s.startswith('BSDF_'):
75            s = s[5:]
76        s = s.lower().replace('_', ' ')
77        props.shaders += (s + ', ')
78
79
80def check_render_engine(props, obs):
81    ob = obs[0]
82    m = None
83
84    e = bpy.context.scene.render.engine
85    mattype = None
86    materials = []
87    shaders = []
88    textures = []
89    props.uv = False
90    props.texture_count = 0
91    props.total_megapixels = 0
92    props.node_count = 0
93    for ob in obs:  # TODO , this is duplicated here for other engines, otherwise this should be more clever.
94        for ms in ob.material_slots:
95            if ms.material is not None:
96                m = ms.material
97                if m.name not in materials:
98                    materials.append(m.name)
99        if ob.type == 'MESH' and len(ob.data.uv_layers) > 0:
100            props.uv = True
101
102    if e == 'BLENDER_RENDER':
103        props.engine = 'BLENDER_INTERNAL'
104    elif e == 'CYCLES':
105
106        props.engine = 'CYCLES'
107
108        for mname in materials:
109            m = bpy.data.materials[mname]
110            if m is not None and m.node_tree is not None:
111                checknodes = m.node_tree.nodes[:]
112                while len(checknodes) > 0:
113                    n = checknodes.pop()
114                    props.node_count +=1
115                    if n.type == 'GROUP':  # dive deeper here.
116                        checknodes.extend(n.node_tree.nodes)
117                    if len(n.outputs) == 1 and n.outputs[0].type == 'SHADER' and n.type != 'GROUP':
118                        if n.type not in shaders:
119                            shaders.append(n.type)
120                    if n.type == 'TEX_IMAGE':
121
122
123                        if n.image is not None and n.image not in textures:
124                            props.is_procedural = False
125                            mattype = 'image based'
126
127                            textures.append(n.image)
128                            props.texture_count += 1
129                            props.total_megapixels += (n.image.size[0] * n.image.size[1])
130
131                            maxres = max(n.image.size[0], n.image.size[1])
132                            props.texture_resolution_max = max(props.texture_resolution_max, maxres)
133                            minres = min(n.image.size[0], n.image.size[1])
134                            if props.texture_resolution_min == 0:
135                                props.texture_resolution_min = minres
136                            else:
137                                props.texture_resolution_min = min(props.texture_resolution_min, minres)
138
139
140        # if mattype == None:
141        #    mattype = 'procedural'
142        # tags['material type'] = mattype
143
144    elif e == 'BLENDER_GAME':
145        props.engine = 'BLENDER_GAME'
146
147    # write to object properties.
148    props.materials = ''
149    props.shaders = ''
150    for m in materials:
151        props.materials += (m + ', ')
152    for s in shaders:
153        if s.startswith('BSDF_'):
154            s = s[5:]
155        s = s.lower()
156        s = s.replace('_', ' ')
157        props.shaders += (s + ', ')
158
159
160def check_printable(props, obs):
161    if len(obs) == 1:
162        check_cls = (
163            ops.Print3DCheckSolid,
164            ops.Print3DCheckIntersections,
165            ops.Print3DCheckDegenerate,
166            ops.Print3DCheckDistorted,
167            ops.Print3DCheckThick,
168            ops.Print3DCheckSharp,
169            # ops.Print3DCheckOverhang,
170        )
171
172        ob = obs[0]
173
174        info = []
175        for cls in check_cls:
176            cls.main_check(ob, info)
177
178        printable = True
179        for item in info:
180            passed = item[0].endswith(' 0')
181            if not passed:
182                print(item[0])
183                printable = False
184
185        props.printable_3d = printable
186
187
188def check_rig(props, obs):
189    for ob in obs:
190        if ob.type == 'ARMATURE':
191            props.rig = True
192
193
194def check_anim(props, obs):
195    animated = False
196    for ob in obs:
197        if ob.animation_data is not None:
198            a = ob.animation_data.action
199            if a is not None:
200                for c in a.fcurves:
201                    if len(c.keyframe_points) > 1:
202                        animated = True
203
204                        # c.keyframe_points.remove(c.keyframe_points[0])
205    if animated:
206        props.animated = True
207
208
209def check_meshprops(props, obs):
210    ''' checks polycount, manifold, mesh parts (not implemented)'''
211    fc = 0
212    fcr = 0
213    tris = 0
214    quads = 0
215    ngons = 0
216    vc = 0
217
218    edges_counts = {}
219    manifold = True
220
221    for ob in obs:
222        if ob.type == 'MESH' or ob.type == 'CURVE':
223            ob_eval = None
224            if ob.type == 'CURVE':
225                # depsgraph = bpy.context.evaluated_depsgraph_get()
226                # object_eval = ob.evaluated_get(depsgraph)
227                mesh = ob.to_mesh()
228            else:
229                mesh = ob.data
230            fco = len(mesh.polygons)
231            fc += fco
232            vc += len(mesh.vertices)
233            fcor = fco
234            for f in mesh.polygons:
235                # face sides counter
236                if len(f.vertices) == 3:
237                    tris += 1
238                elif len(f.vertices) == 4:
239                    quads += 1
240                elif len(f.vertices) > 4:
241                    ngons += 1
242
243                # manifold counter
244                for i, v in enumerate(f.vertices):
245                    v1 = f.vertices[i - 1]
246                    e = (min(v, v1), max(v, v1))
247                    edges_counts[e] = edges_counts.get(e, 0) + 1
248
249            # all meshes have to be manifold for this to work.
250            manifold = manifold and not any(i in edges_counts.values() for i in [0, 1, 3, 4])
251
252            for m in ob.modifiers:
253                if m.type == 'SUBSURF' or m.type == 'MULTIRES':
254                    fcor *= 4 ** m.render_levels
255                if m.type == 'SOLIDIFY':  # this is rough estimate, not to waste time with evaluating all nonmanifold edges
256                    fcor *= 2
257                if m.type == 'ARRAY':
258                    fcor *= m.count
259                if m.type == 'MIRROR':
260                    fcor *= 2
261                if m.type == 'DECIMATE':
262                    fcor *= m.ratio
263            fcr += fcor
264
265            if ob_eval:
266                ob_eval.to_mesh_clear()
267
268    # write out props
269    props.face_count = fc
270    props.face_count_render = fcr
271    # print(tris, quads, ngons)
272    if quads > 0 and tris == 0 and ngons == 0:
273        props.mesh_poly_type = 'QUAD'
274    elif quads > tris and quads > ngons:
275        props.mesh_poly_type = 'QUAD_DOMINANT'
276    elif tris > quads and tris > quads:
277        props.mesh_poly_type = 'TRI_DOMINANT'
278    elif quads == 0 and tris > 0 and ngons == 0:
279        props.mesh_poly_type = 'TRI'
280    elif ngons > quads and ngons > tris:
281        props.mesh_poly_type = 'NGON'
282    else:
283        props.mesh_poly_type = 'OTHER'
284
285    props.manifold = manifold
286
287
288def countObs(props, obs):
289    ob_types = {}
290    count = len(obs)
291    for ob in obs:
292        otype = ob.type.lower()
293        ob_types[otype] = ob_types.get(otype, 0) + 1
294    props.object_count = count
295
296
297def check_modifiers(props, obs):
298    # modif_mapping = {
299    # }
300    modifiers = []
301    for ob in obs:
302        for m in ob.modifiers:
303            mtype = m.type
304            mtype = mtype.replace('_', ' ')
305            mtype = mtype.lower()
306            # mtype = mtype.capitalize()
307            if mtype not in modifiers:
308                modifiers.append(mtype)
309            if m.type == 'SMOKE':
310                if m.smoke_type == 'FLOW':
311                    smt = m.flow_settings.smoke_flow_type
312                    if smt == 'BOTH' or smt == 'FIRE':
313                        modifiers.append('fire')
314
315    # for mt in modifiers:
316    effectmodifiers = ['soft body', 'fluid simulation', 'particle system', 'collision', 'smoke', 'cloth',
317                       'dynamic paint']
318    for m in modifiers:
319        if m in effectmodifiers:
320            props.simulation = True
321    if ob.rigid_body is not None:
322        props.simulation = True
323        modifiers.append('rigid body')
324    finalstr = ''
325    for m in modifiers:
326        finalstr += m
327        finalstr += ','
328    props.modifiers = finalstr
329
330
331def get_autotags():
332    """ call all analysis functions """
333    ui = bpy.context.scene.blenderkitUI
334    if ui.asset_type == 'MODEL':
335        ob = utils.get_active_model()
336        obs = utils.get_hierarchy(ob)
337        props = ob.blenderkit
338        if props.name == "":
339            props.name = ob.name
340
341        # reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
342        props.texture_resolution_max = 0
343        props.texture_resolution_min = 0
344        # disabled printing checking, some 3d print addon bug.
345        # check_printable( props, obs)
346        check_render_engine(props, obs)
347
348        dim, bbox_min, bbox_max = utils.get_dimensions(obs)
349        props.dimensions = dim
350        props.bbox_min = bbox_min
351        props.bbox_max = bbox_max
352
353        check_rig(props, obs)
354        check_anim(props, obs)
355        check_meshprops(props, obs)
356        check_modifiers(props, obs)
357        countObs(props, obs)
358    elif ui.asset_type == 'MATERIAL':
359        # reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
360
361        mat = utils.get_active_asset()
362        props = mat.blenderkit
363        props.texture_resolution_max = 0
364        props.texture_resolution_min = 0
365        check_material(props, mat)
366
367
368class AutoFillTags(bpy.types.Operator):
369    """Fill tags for asset. Now run before upload, no need to interact from user side."""
370    bl_idname = "object.blenderkit_auto_tags"
371    bl_label = "Generate Auto Tags for BlenderKit"
372    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
373
374    @classmethod
375    def poll(cls, context):
376        return bpy.context.view_layer.objects.active is not None
377
378    def execute(self, context):
379        get_autotags()
380        return {'FINISHED'}
381
382
383def register_asset_inspector():
384    bpy.utils.register_class(AutoFillTags)
385
386
387def unregister_asset_inspector():
388    bpy.utils.unregister_class(AutoFillTags)
389
390
391if __name__ == "__main__":
392    register()
393