""" Blender API for querying mesh data. Animation data is also handled here since Three.js associates the animation (skeletal, morph targets) with the geometry nodes. """ import operator import re from bpy import data, types, context from . import material, texture, animation from . import object as object_ from .. import constants, utilities, logger, exceptions def _mesh(func): """ :param func: """ def inner(name, *args, **kwargs): """ :param name: :param *args: :param **kwargs: """ if isinstance(name, types.Mesh): mesh = name else: mesh = data.meshes[name] return func(mesh, *args, **kwargs) return inner @_mesh def skeletal_animation(mesh, options): """ :param mesh: :param options: :rtype: [] """ logger.debug("mesh.animation(%s, %s)", mesh, options) armature = _armature(mesh) if not armature: logger.warning("No armature found (%s)", mesh) return [] anim_type = options.get(constants.ANIMATION) # pose_position = armature.data.pose_position dispatch = { constants.POSE: animation.pose_animation, constants.REST: animation.rest_animation } func = dispatch[anim_type] # armature.data.pose_position = anim_type.upper() animations = func(armature, options) # armature.data.pose_position = pose_position return animations @_mesh def bones(mesh, options): """ :param mesh: :param options: :rtype: [], {} """ logger.debug("mesh.bones(%s)", mesh) armature = _armature(mesh) if not armature: return [], {} anim_type = options.get(constants.ANIMATION) # pose_position = armature.data.pose_position if anim_type == constants.OFF: logger.info("Animation type not set, defaulting " "to using REST position for the armature.") func = _rest_bones # armature.data.pose_position = "REST" else: dispatch = { constants.REST: _rest_bones, constants.POSE: _pose_bones } logger.info("Using %s for the armature", anim_type) func = dispatch[anim_type] # armature.data.pose_position = anim_type.upper() bones_, bone_map = func(armature) # armature.data.pose_position = pose_position return (bones_, bone_map) @_mesh def buffer_normal(mesh): """ :param mesh: :rtype: [] """ normals_ = [] for face in mesh.tessfaces: vert_count = len(face.vertices) if vert_count is not 3: msg = "Non-triangulated face detected" raise exceptions.BufferGeometryError(msg) for vertex_index in face.vertices: normal = mesh.vertices[vertex_index].normal vector = (normal.x, normal.y, normal.z) if face.use_smooth else (face.normal.x, face.normal.y, face.normal.z) normals_.extend(vector) return normals_ @_mesh def buffer_position(mesh): """ :param mesh: :rtype: [] """ position = [] for face in mesh.tessfaces: vert_count = len(face.vertices) if vert_count is not 3: msg = "Non-triangulated face detected" raise exceptions.BufferGeometryError(msg) for vertex_index in face.vertices: vertex = mesh.vertices[vertex_index] vector = (vertex.co.x, vertex.co.y, vertex.co.z) position.extend(vector) return position @_mesh def buffer_uv(mesh, layer=0): """ :param mesh: :param layer: (Default value = 0) :rtype: [] """ uvs_ = [] if len(mesh.uv_layers) <= layer: return uvs_ for uv_data in mesh.uv_layers[layer].data: uv_tuple = (uv_data.uv[0], uv_data.uv[1]) uvs_.extend(uv_tuple) return uvs_ @_mesh def extra_vertex_groups(mesh, patterns_string): """ Returns (name,index) tuples for the extra (non-skinning) vertex groups matching the given patterns. The patterns are comma-separated where the star character can be used as a wildcard character sequence. :param mesh: :param patterns_string: :rtype: [] """ logger.debug("mesh._extra_vertex_groups(%s)", mesh) pattern_re = None extra_vgroups = [] if not patterns_string.strip(): return extra_vgroups armature = _armature(mesh) obj = object_.objects_using_mesh(mesh)[0] for vgroup_index, vgroup in enumerate(obj.vertex_groups): # Skip bone weights: vgroup_name = vgroup.name if armature: is_bone_weight = False for bone in armature.pose.bones: if bone.name == vgroup_name: is_bone_weight = True break if is_bone_weight: continue if pattern_re is None: # Translate user-friendly patterns to a regular expression: # Join the whitespace-stripped, initially comma-separated # entries to alternatives. Escape all characters except # the star and replace that one with '.*?'. pattern_re = '^(?:' + '|'.join( map(lambda entry: '.*?'.join(map(re.escape, entry.strip().split('*'))), patterns_string.split(','))) + ')$' if not re.match(pattern_re, vgroup_name): continue extra_vgroups.append((vgroup_name, vgroup_index)) return extra_vgroups @_mesh def vertex_group_data(mesh, index): """ Return vertex group data for each vertex. Vertices not in the group get a zero value. :param mesh: :param index: """ group_data = [] for vertex in mesh.vertices: weight = None for group in vertex.groups: if group.group == index: weight = group.weight group_data.append(weight or 0.0) return group_data @_mesh def buffer_vertex_group_data(mesh, index): """ Return vertex group data for each deindexed vertex. Vertices not in the group get a zero value. :param mesh: :param index: """ group_data = [] for face in mesh.tessfaces: for vertex_index in face.vertices: vertex = mesh.vertices[vertex_index] weight = None for group in vertex.groups: if group.group == index: weight = group.weight group_data.append(weight or 0.0) return group_data @_mesh def faces(mesh, options, material_list=None): """ :param mesh: :param options: :param material_list: (Default value = None) """ logger.debug("mesh.faces(%s, %s, materials=%s)", mesh, options, materials) material_list = material_list or [] vertex_uv = len(mesh.uv_textures) > 0 has_colors = len(mesh.vertex_colors) > 0 logger.info("Has UVs = %s", vertex_uv) logger.info("Has vertex colours = %s", has_colors) opt_colours = options[constants.COLORS] and has_colors opt_uvs = options[constants.UVS] and vertex_uv opt_materials = options.get(constants.FACE_MATERIALS) opt_normals = options[constants.NORMALS] logger.debug("Vertex colours enabled = %s", opt_colours) logger.debug("UVS enabled = %s", opt_uvs) logger.debug("Materials enabled = %s", opt_materials) logger.debug("Normals enabled = %s", opt_normals) uv_indices = _uvs(mesh)[1] if opt_uvs else None vertex_normals = _normals(mesh) if opt_normals else None vertex_colours = vertex_colors(mesh) if opt_colours else None faces_data = [] colour_indices = {} if vertex_colours: logger.debug("Indexing colours") for index, colour in enumerate(vertex_colours): colour_indices[str(colour)] = index normal_indices = {} if vertex_normals: logger.debug("Indexing normals") for index, normal in enumerate(vertex_normals): normal = (normal[0], normal[2], -normal[1]) normal_indices[str(normal)] = index logger.info("Parsing %d faces", len(mesh.tessfaces)) for face in mesh.tessfaces: vert_count = len(face.vertices) if vert_count not in (3, 4): logger.error("%d vertices for face %d detected", vert_count, face.index) raise exceptions.NGonError("ngons are not supported") mat_index = face.material_index is not None and opt_materials mask = { constants.QUAD: vert_count is 4, constants.MATERIALS: mat_index, constants.UVS: False, constants.NORMALS: False, constants.COLORS: False } face_data = [] face_data.extend([v for v in face.vertices]) if mask[constants.MATERIALS]: for mat_index, mat in enumerate(material_list): if mat[constants.DBG_INDEX] == face.material_index: face_data.append(mat_index) break else: logger.warning("Could not map the material index " "for face %d" % face.index) face_data.append(0) # default to index zero if there's a bad material if uv_indices: for index, uv_layer in enumerate(uv_indices): layer = mesh.tessface_uv_textures[index] for uv_data in layer.data[face.index].uv: uv_tuple = (uv_data[0], uv_data[1]) uv_index = uv_layer[str(uv_tuple)] face_data.append(uv_index) mask[constants.UVS] = True if vertex_normals: for vertex in face.vertices: normal = mesh.vertices[vertex].normal normal = (normal.x, normal.z, -normal.y) if face.use_smooth else (face.normal.x, face.normal.z, -face.normal.y) face_data.append(normal_indices[str(normal)]) mask[constants.NORMALS] = True if vertex_colours: colours = mesh.tessface_vertex_colors.active.data[face.index] for each in (colours.color1, colours.color2, colours.color3): each = utilities.rgb2int(each) face_data.append(colour_indices[str(each)]) mask[constants.COLORS] = True if mask[constants.QUAD]: colour = utilities.rgb2int(colours.color4) face_data.append(colour_indices[str(colour)]) face_data.insert(0, utilities.bit_mask(mask)) faces_data.extend(face_data) return faces_data @_mesh def morph_targets(mesh, options): """ :param mesh: :param options: """ logger.debug("mesh.morph_targets(%s, %s)", mesh, options) obj = object_.objects_using_mesh(mesh)[0] original_frame = context.scene.frame_current frame_step = options.get(constants.FRAME_STEP, 1) scene_frames = range(context.scene.frame_start, context.scene.frame_end+1, frame_step) morphs = [] for frame in scene_frames: logger.info("Processing data at frame %d", frame) context.scene.frame_set(frame, 0.0) morphs.append([]) vertices_ = object_.extract_mesh(obj, options).vertices[:] for vertex in vertices_: morphs[-1].extend([vertex.co.x, vertex.co.y, vertex.co.z]) context.scene.frame_set(original_frame, 0.0) morphs_detected = False for index, each in enumerate(morphs): if index is 0: continue morphs_detected = morphs[index-1] != each if morphs_detected: logger.info("Valid morph target data detected") break else: logger.info("No valid morph data detected") return [] manifest = [] for index, morph in enumerate(morphs): manifest.append({ constants.NAME: 'animation_%06d' % index, constants.VERTICES: morph }) return manifest @_mesh def blend_shapes(mesh, options): """ :param mesh: :param options: """ logger.debug("mesh.blend_shapes(%s, %s)", mesh, options) manifest = [] if mesh.shape_keys: logger.info("mesh.blend_shapes -- there's shape keys") key_blocks = mesh.shape_keys.key_blocks for key in key_blocks.keys()[1:]: # skip "Basis" logger.info("mesh.blend_shapes -- key %s", key) morph = [] for d in key_blocks[key].data: co = d.co morph.extend([co.x, co.y, co.z]) manifest.append({ constants.NAME: key, constants.VERTICES: morph }) else: logger.debug("No valid blend_shapes detected") return manifest @_mesh def animated_blend_shapes(mesh, name, options): """ :param mesh: :param options: """ # let filter the name to only keep the node's name # the two cases are '%sGeometry' and '%sGeometry.%d', and we want %s name = re.search("^(.*)Geometry(\..*)?$", name).group(1) logger.debug("mesh.animated_blend_shapes(%s, %s)", mesh, options) tracks = [] shp = mesh.shape_keys animCurves = shp.animation_data if animCurves: animCurves = animCurves.action.fcurves for key in shp.key_blocks.keys()[1:]: # skip "Basis" key_name = name+".morphTargetInfluences["+key+"]" found_animation = False data_path = 'key_blocks["'+key+'"].value' values = [] if animCurves: for fcurve in animCurves: if fcurve.data_path == data_path: for xx in fcurve.keyframe_points: values.append({ "time": xx.co.x, "value": xx.co.y }) found_animation = True break # no need to continue if found_animation: tracks.append({ constants.NAME: key_name, constants.TYPE: "number", constants.KEYS: values }); return tracks @_mesh def materials(mesh, options): """ :param mesh: :param options: """ logger.debug("mesh.materials(%s, %s)", mesh, options) # sanity check if not mesh.materials: return [] indices = [] for face in mesh.tessfaces: if face.material_index not in indices: indices.append(face.material_index) material_sets = [(mesh.materials[index], index) for index in indices] materials_ = [] maps = options.get(constants.MAPS) mix = options.get(constants.MIX_COLORS) use_colors = options.get(constants.COLORS) logger.info("Colour mix is set to %s", mix) logger.info("Vertex colours set to %s", use_colors) for mat, index in material_sets: if mat == None: # undefined material for a specific index is skipped continue try: dbg_color = constants.DBG_COLORS[index] except IndexError: dbg_color = constants.DBG_COLORS[0] logger.info("Compiling attributes for %s", mat.name) attributes = { constants.COLOR_EMISSIVE: material.emissive_color(mat), constants.SHADING: material.shading(mat), constants.OPACITY: material.opacity(mat), constants.TRANSPARENT: material.transparent(mat), constants.VISIBLE: material.visible(mat), constants.WIREFRAME: material.wireframe(mat), constants.BLENDING: material.blending(mat), constants.DEPTH_TEST: material.depth_test(mat), constants.DEPTH_WRITE: material.depth_write(mat), constants.DBG_NAME: mat.name, constants.DBG_COLOR: dbg_color, constants.DBG_INDEX: index } if use_colors: colors = material.use_vertex_colors(mat) attributes[constants.VERTEX_COLORS] = colors if (use_colors and mix) or (not use_colors): colors = material.diffuse_color(mat) attributes[constants.COLOR_DIFFUSE] = colors if attributes[constants.SHADING] == constants.PHONG: logger.info("Adding specular attributes") attributes.update({ constants.SPECULAR_COEF: material.specular_coef(mat), constants.COLOR_SPECULAR: material.specular_color(mat) }) if mesh.show_double_sided: logger.info("Double sided is on") attributes[constants.DOUBLE_SIDED] = True materials_.append(attributes) if not maps: continue diffuse = _diffuse_map(mat) if diffuse: logger.info("Diffuse map found") attributes.update(diffuse) light = _light_map(mat) if light: logger.info("Light map found") attributes.update(light) specular = _specular_map(mat) if specular: logger.info("Specular map found") attributes.update(specular) if attributes[constants.SHADING] == constants.PHONG: normal = _normal_map(mat) if normal: logger.info("Normal map found") attributes.update(normal) bump = _bump_map(mat) if bump: logger.info("Bump map found") attributes.update(bump) return materials_ @_mesh def normals(mesh): """ :param mesh: :rtype: [] """ logger.debug("mesh.normals(%s)", mesh) normal_vectors = [] for vector in _normals(mesh): vector = (vector[0], vector[2], -vector[1]) normal_vectors.extend(vector) return normal_vectors @_mesh def skin_weights(mesh, bone_map, influences, anim_type): """ :param mesh: :param bone_map: :param influences: :param anim_type """ logger.debug("mesh.skin_weights(%s)", mesh) return _skinning_data(mesh, bone_map, influences, anim_type, 1) @_mesh def skin_indices(mesh, bone_map, influences, anim_type): """ :param mesh: :param bone_map: :param influences: :param anim_type """ logger.debug("mesh.skin_indices(%s)", mesh) return _skinning_data(mesh, bone_map, influences, anim_type, 0) @_mesh def texture_registration(mesh): """ :param mesh: """ logger.debug("mesh.texture_registration(%s)", mesh) materials_ = mesh.materials or [] registration = {} funcs = ( (constants.MAP_DIFFUSE, material.diffuse_map), (constants.SPECULAR_MAP, material.specular_map), (constants.LIGHT_MAP, material.light_map), (constants.BUMP_MAP, material.bump_map), (constants.NORMAL_MAP, material.normal_map) ) def _registration(file_path, file_name): """ :param file_path: :param file_name: """ return { 'file_path': file_path, 'file_name': file_name, 'maps': [] } logger.info("found %d materials", len(materials_)) for mat in materials_: for (key, func) in funcs: tex = func(mat) if tex is None: continue logger.info("%s has texture %s", key, tex.name) file_path = texture.file_path(tex) file_name = texture.file_name(tex) reg = registration.setdefault( utilities.hash(file_path), _registration(file_path, file_name)) reg["maps"].append(key) return registration @_mesh def uvs(mesh): """ :param mesh: :rtype: [] """ logger.debug("mesh.uvs(%s)", mesh) uvs_ = [] for layer in _uvs(mesh)[0]: uvs_.append([]) logger.info("Parsing UV layer %d", len(uvs_)) for pair in layer: uvs_[-1].extend(pair) return uvs_ @_mesh def vertex_colors(mesh): """ :param mesh: """ logger.debug("mesh.vertex_colors(%s)", mesh) vertex_colours = [] try: vertex_colour = mesh.tessface_vertex_colors.active.data except AttributeError: logger.info("No vertex colours found") return for face in mesh.tessfaces: colours = (vertex_colour[face.index].color1, vertex_colour[face.index].color2, vertex_colour[face.index].color3, vertex_colour[face.index].color4) for colour in colours: colour = utilities.rgb2int((colour.r, colour.g, colour.b)) if colour not in vertex_colours: vertex_colours.append(colour) return vertex_colours @_mesh def vertices(mesh): """ :param mesh: :rtype: [] """ logger.debug("mesh.vertices(%s)", mesh) vertices_ = [] for vertex in mesh.vertices: vertices_.extend((vertex.co.x, vertex.co.y, vertex.co.z)) return vertices_ def _normal_map(mat): """ :param mat: """ tex = material.normal_map(mat) if tex is None: return logger.info("Found normal texture map %s", tex.name) normal = { constants.MAP_NORMAL: texture.file_name(tex), constants.MAP_NORMAL_FACTOR: material.normal_scale(mat), constants.MAP_NORMAL_ANISOTROPY: texture.anisotropy(tex), constants.MAP_NORMAL_WRAP: texture.wrap(tex), constants.MAP_NORMAL_REPEAT: texture.repeat(tex) } return normal def _bump_map(mat): """ :param mat: """ tex = material.bump_map(mat) if tex is None: return logger.info("Found bump texture map %s", tex.name) bump = { constants.MAP_BUMP: texture.file_name(tex), constants.MAP_BUMP_ANISOTROPY: texture.anisotropy(tex), constants.MAP_BUMP_WRAP: texture.wrap(tex), constants.MAP_BUMP_REPEAT: texture.repeat(tex), constants.MAP_BUMP_SCALE: material.bump_scale(mat), } return bump def _specular_map(mat): """ :param mat: """ tex = material.specular_map(mat) if tex is None: return logger.info("Found specular texture map %s", tex.name) specular = { constants.MAP_SPECULAR: texture.file_name(tex), constants.MAP_SPECULAR_ANISOTROPY: texture.anisotropy(tex), constants.MAP_SPECULAR_WRAP: texture.wrap(tex), constants.MAP_SPECULAR_REPEAT: texture.repeat(tex) } return specular def _light_map(mat): """ :param mat: """ tex = material.light_map(mat) if tex is None: return logger.info("Found light texture map %s", tex.name) light = { constants.MAP_LIGHT: texture.file_name(tex), constants.MAP_LIGHT_ANISOTROPY: texture.anisotropy(tex), constants.MAP_LIGHT_WRAP: texture.wrap(tex), constants.MAP_LIGHT_REPEAT: texture.repeat(tex) } return light def _diffuse_map(mat): """ :param mat: """ tex = material.diffuse_map(mat) if tex is None: return logger.info("Found diffuse texture map %s", tex.name) diffuse = { constants.MAP_DIFFUSE: texture.file_name(tex), constants.MAP_DIFFUSE_ANISOTROPY: texture.anisotropy(tex), constants.MAP_DIFFUSE_WRAP: texture.wrap(tex), constants.MAP_DIFFUSE_REPEAT: texture.repeat(tex) } return diffuse def _normals(mesh): """ :param mesh: :rtype: [] """ vectors = [] vectors_ = {} for face in mesh.tessfaces: for vertex_index in face.vertices: normal = mesh.vertices[vertex_index].normal vector = (normal.x, normal.y, normal.z) if face.use_smooth else (face.normal.x, face.normal.y, face.normal.z) str_vec = str(vector) try: vectors_[str_vec] except KeyError: vectors.append(vector) vectors_[str_vec] = True return vectors def _uvs(mesh): """ :param mesh: :rtype: [[], ...], [{}, ...] """ uv_layers = [] uv_indices = [] for layer in mesh.uv_layers: uv_layers.append([]) uv_indices.append({}) index = 0 for uv_data in layer.data: uv_tuple = (uv_data.uv[0], uv_data.uv[1]) uv_key = str(uv_tuple) try: uv_indices[-1][uv_key] except KeyError: uv_indices[-1][uv_key] = index uv_layers[-1].append(uv_tuple) index += 1 return uv_layers, uv_indices def _armature(mesh): """ :param mesh: """ obj = object_.objects_using_mesh(mesh)[0] armature = obj.find_armature() if armature: logger.info("Found armature %s for %s", armature.name, obj.name) else: logger.info("Found no armature for %s", obj.name) return armature def _skinning_data(mesh, bone_map, influences, anim_type, array_index): """ :param mesh: :param bone_map: :param influences: :param array_index: :param anim_type """ armature = _armature(mesh) manifest = [] if not armature: return manifest # armature bones here based on type if anim_type == constants.OFF or anim_type == constants.REST: armature_bones = armature.data.bones else: # POSE mode armature_bones = armature.pose.bones obj = object_.objects_using_mesh(mesh)[0] logger.debug("Skinned object found %s", obj.name) for vertex in mesh.vertices: bone_array = [] for group in vertex.groups: bone_array.append((group.group, group.weight)) bone_array.sort(key=operator.itemgetter(1), reverse=True) for index in range(influences): if index >= len(bone_array): manifest.append(0) continue name = obj.vertex_groups[bone_array[index][0]].name for bone_index, bone in enumerate(armature_bones): if bone.name != name: continue if array_index is 0: entry = bone_map.get(bone_index, -1) else: entry = bone_array[index][1] manifest.append(entry) break else: manifest.append(0) return manifest def _pose_bones(armature): """ :param armature: :rtype: [], {} """ bones_ = [] bone_map = {} bone_count = 0 armature_matrix = armature.matrix_world for bone_count, pose_bone in enumerate(armature.pose.bones): armature_bone = pose_bone.bone bone_index = None if armature_bone.parent is None: bone_matrix = armature_matrix * armature_bone.matrix_local bone_index = -1 else: parent_bone = armature_bone.parent parent_matrix = armature_matrix * parent_bone.matrix_local bone_matrix = armature_matrix * armature_bone.matrix_local bone_matrix = parent_matrix.inverted() * bone_matrix bone_index = index = 0 for pose_parent in armature.pose.bones: armature_parent = pose_parent.bone.name if armature_parent == parent_bone.name: bone_index = index index += 1 bone_map[bone_count] = bone_count pos, rot, scl = bone_matrix.decompose() bones_.append({ constants.PARENT: bone_index, constants.NAME: armature_bone.name, constants.POS: (pos.x, pos.z, -pos.y), constants.ROTQ: (rot.x, rot.z, -rot.y, rot.w), constants.SCL: (scl.x, scl.z, scl.y) }) return bones_, bone_map def _rest_bones(armature): """ :param armature: :rtype: [], {} """ bones_ = [] bone_map = {} bone_count = 0 bone_index_rel = 0 for bone in armature.data.bones: logger.info("Parsing bone %s", bone.name) if not bone.use_deform: logger.debug("Ignoring bone %s at: %d", bone.name, bone_index_rel) continue if bone.parent is None: bone_pos = bone.head_local bone_index = -1 else: bone_pos = bone.head_local - bone.parent.head_local bone_index = 0 index = 0 for parent in armature.data.bones: if parent.name == bone.parent.name: bone_index = bone_map.get(index) index += 1 bone_world_pos = armature.matrix_world * bone_pos x_axis = bone_world_pos.x y_axis = bone_world_pos.z z_axis = -bone_world_pos.y logger.debug("Adding bone %s at: %s, %s", bone.name, bone_index, bone_index_rel) bone_map[bone_count] = bone_index_rel bone_index_rel += 1 # @TODO: the rotq probably should not have these # hard coded values bones_.append({ constants.PARENT: bone_index, constants.NAME: bone.name, constants.POS: (x_axis, y_axis, z_axis), constants.ROTQ: (0, 0, 0, 1) }) bone_count += 1 return (bones_, bone_map)