diff options
Diffstat (limited to 'io_scene_gltf2/blender/exp')
28 files changed, 1124 insertions, 325 deletions
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py index 812db3f9..96f97af1 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py @@ -20,7 +20,6 @@ RENDERABLE = 'gltf_renderable' ACTIVE_COLLECTION = 'gltf_active_collection' SKINS = 'gltf_skins' DEF_BONES_ONLY = 'gltf_def_bones' -DISPLACEMENT = 'gltf_displacement' FORCE_SAMPLING = 'gltf_force_sampling' FRAME_RANGE = 'gltf_frame_range' FRAME_STEP = 'gltf_frame_step' diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index d4f34126..93947acb 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -39,6 +39,18 @@ def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups if export_settings[gltf2_blender_export_keys.COLORS]: color_max = len(blender_mesh.vertex_colors) + colors_attributes = [] + rendered_color_idx = blender_mesh.attributes.render_color_index + + if color_max > 0: + colors_attributes.append(rendered_color_idx) + # Then find other ones + colors_attributes.extend([ + i for i in range(len(blender_mesh.color_attributes)) if i != rendered_color_idx \ + and blender_mesh.vertex_colors.find(blender_mesh.color_attributes[i].name) != -1 + ]) + + armature = None skin = None if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]: @@ -110,7 +122,7 @@ def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups dot_fields += [('tx', np.float32), ('ty', np.float32), ('tz', np.float32), ('tw', np.float32)] for uv_i in range(tex_coord_max): dot_fields += [('uv%dx' % uv_i, np.float32), ('uv%dy' % uv_i, np.float32)] - for col_i in range(color_max): + for col_i, _ in enumerate(colors_attributes): dot_fields += [ ('color%dr' % col_i, np.float32), ('color%dg' % col_i, np.float32), @@ -163,8 +175,12 @@ def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups dots['uv%dy' % uv_i] = uvs[:, 1] del uvs - for col_i in range(color_max): - colors = __get_colors(blender_mesh, col_i) + colors_types = [] + for col_i, blender_col_i in enumerate(colors_attributes): + colors, colors_type, domain = __get_colors(blender_mesh, col_i, blender_col_i) + if domain == "POINT": + colors = colors[dots['vertex_index']] + colors_types.append(colors_type) dots['color%dr' % col_i] = colors[:, 0] dots['color%dg' % col_i] = colors[:, 1] dots['color%db' % col_i] = colors[:, 2] @@ -251,13 +267,16 @@ def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups uvs[:, 1] = prim_dots['uv%dy' % tex_coord_i] attributes['TEXCOORD_%d' % tex_coord_i] = uvs - for color_i in range(color_max): + for color_i, _ in enumerate(colors_attributes): colors = np.empty((len(prim_dots), 4), dtype=np.float32) colors[:, 0] = prim_dots['color%dr' % color_i] colors[:, 1] = prim_dots['color%dg' % color_i] colors[:, 2] = prim_dots['color%db' % color_i] colors[:, 3] = prim_dots['color%da' % color_i] - attributes['COLOR_%d' % color_i] = colors + attributes['COLOR_%d' % color_i] = {} + attributes['COLOR_%d' % color_i]["data"] = colors + + attributes['COLOR_%d' % color_i]["norm"] = colors_types[color_i] == "BYTE_COLOR" if skin: joints = [[] for _ in range(num_joint_sets)] @@ -525,13 +544,15 @@ def __get_uvs(blender_mesh, uv_i): return uvs -def __get_colors(blender_mesh, color_i): - colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) - layer = blender_mesh.vertex_colors[color_i] - blender_mesh.color_attributes[layer.name].data.foreach_get('color', colors) - colors = colors.reshape(len(blender_mesh.loops), 4) +def __get_colors(blender_mesh, color_i, blender_color_i): + if blender_mesh.color_attributes[blender_color_i].domain == "POINT": + colors = np.empty(len(blender_mesh.vertices) * 4, dtype=np.float32) #POINT + else: + colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) #CORNER + blender_mesh.color_attributes[blender_color_i].data.foreach_get('color', colors) + colors = colors.reshape(-1, 4) # colors are already linear, no need to switch color space - return colors + return colors, blender_mesh.color_attributes[blender_color_i].data_type, blender_mesh.color_attributes[blender_color_i].domain def __get_bone_data(blender_mesh, skin, blender_vertex_groups): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py index 6153bc33..721ef115 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -59,6 +59,8 @@ def __gather_scene(blender_scene, export_settings): # Now, we can filter tree if needed vtree.filter() + vtree.variants_reset_to_original() + export_user_extensions('vtree_after_filter_hook', export_settings, vtree) export_settings['vtree'] = vtree @@ -94,9 +96,12 @@ def __gather_animations(blender_scene, export_settings): if export_settings['gltf_nla_strips'] is False: # Fake an animation with all animations of the scene merged_tracks = {} - merged_tracks['Animation'] = [] + merged_tracks_name = 'Animation' + if(len(export_settings['gltf_nla_strips_merged_animation_name']) > 0): + merged_tracks_name = export_settings['gltf_nla_strips_merged_animation_name'] + merged_tracks[merged_tracks_name] = [] for idx, animation in enumerate(animations): - merged_tracks['Animation'].append(idx) + merged_tracks[merged_tracks_name].append(idx) to_delete_idx = [] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py index 98ae8b82..3e67f1f7 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py @@ -37,7 +37,7 @@ def gather_animation_channels(obj_uuid: int, if blender_action.use_frame_range is True: bake_range_start = blender_action.frame_start bake_range_end = blender_action.frame_end - force_range = True # keyframe_points is read-only, we cant restrict here + force_range = True # keyframe_points is read-only, we can't restrict here else: groups = __get_channel_groups(blender_action, blender_object, export_settings) # Note: channels has some None items only for SK if some SK are not animated diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py index e1ed19ea..53d78945 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -398,11 +398,17 @@ def gather_keyframes(blender_obj_uuid: str, key.set_first_tangent() else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately - # use a point at t-1 to define the tangent. This allows the tangent control point to be transformed - # normally + # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed + # normally, but only works for locally linear transformation. The more non-linear a transform, the + # more imprecise this method is. + # We could use any other (v1, t1) for which (v1 - v0) / (t1 - t0) equals the tangent. By using t+1 + # for both in and out tangents, we guarantee that (even if there are errors or numerical imprecisions) + # symmetrical control points translate to symmetrical tangents. + # Note: I am not sure that linearity is never broken with quaternions and their normalization. + # Especially at sign swap it might occur that the value gets negated but the control point not. + # I have however not once encountered an issue with this. key.in_tangent = [ - c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] - ) / (frame - frames[i - 1])) + c.keyframe_points[i].co[1] + (c.keyframe_points[i].handle_left[1] - c.keyframe_points[i].co[1]) / (c.keyframe_points[i].handle_left[0] - c.keyframe_points[i].co[0]) for c in channels if c is not None ] # Construct the out tangent @@ -410,12 +416,10 @@ def gather_keyframes(blender_obj_uuid: str, # end out-tangent should become all zero key.set_last_tangent() else: - # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately - # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed - # normally + # otherwise construct an in tangent coordinate from the keyframes control points. + # This happens the same way how in tangents are handled above. key.out_tangent = [ - c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] - ) / (frames[i + 1] - frame)) + c.keyframe_points[i].co[1] + (c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]) / (c.keyframe_points[i].handle_right[0] - c.keyframe_points[i].co[0]) for c in channels if c is not None ] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py index 1ee98a29..5c8011ed 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py @@ -414,6 +414,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], transform = parent_inverse values = [] + fps = bpy.context.scene.render.fps for keyframe in keyframes: # Transform the data and build gltf control points value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction) @@ -426,11 +427,11 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform, need_rotation_correction) if is_yup and blender_object_if_armature is None: in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath) - # the tangent in glTF is relative to the keyframe value + # the tangent in glTF is relative to the keyframe value and uses seconds if not isinstance(value, list): - in_tangent = value - in_tangent + in_tangent = fps * (in_tangent - value) else: - in_tangent = [value[i] - in_tangent[i] for i in range(len(value))] + in_tangent = [fps * (in_tangent[i] - value[i]) for i in range(len(value))] keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value # append if keyframe.out_tangent is not None: @@ -438,11 +439,11 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform, need_rotation_correction) if is_yup and blender_object_if_armature is None: out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath) - # the tangent in glTF is relative to the keyframe value + # the tangent in glTF is relative to the keyframe value and uses seconds if not isinstance(value, list): - out_tangent = value - out_tangent + out_tangent = fps * (out_tangent - value) else: - out_tangent = [value[i] - out_tangent[i] for i in range(len(value))] + out_tangent = [fps * (out_tangent[i] - value[i]) for i in range(len(value))] keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent) # append values += keyframe_value diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py index 20a919dc..bdb2ee00 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py @@ -64,21 +64,24 @@ def gather_animations( obj_uuid: int, # No TRS animation are found for this object. # But we need to bake, in case we export selection # (Only when force sampling is ON) - # If force sampling is OFF, can lead to inconsistant export anyway + # If force sampling is OFF, can lead to inconsistent export anyway if export_settings['gltf_selected'] is True and blender_object.type != "ARMATURE" and export_settings['gltf_force_sampling'] is True: - channels = __gather_channels_baked(obj_uuid, export_settings) - if channels is not None: - animation = gltf2_io.Animation( - channels=channels, - extensions=None, # as other animations - extras=None, # Because there is no animation to get extras from - name=blender_object.name, # Use object name as animation name - samplers=[] - ) - - __link_samplers(animation, export_settings) - if animation is not None: - animations.append(animation) + # We also have to check if this is a skinned mesh, because we don't have to force animation baking on this case + # (skinned meshes TRS must be ignored, says glTF specification) + if export_settings['vtree'].nodes[obj_uuid].skin is None: + channels = __gather_channels_baked(obj_uuid, export_settings) + if channels is not None: + animation = gltf2_io.Animation( + channels=channels, + extensions=None, # as other animations + extras=None, # Because there is no animation to get extras from + name=blender_object.name, # Use object name as animation name + samplers=[] + ) + + __link_samplers(animation, export_settings) + if animation is not None: + animations.append(animation) elif export_settings['gltf_selected'] is True and blender_object.type == "ARMATURE": # We need to bake all bones. Because some bone can have some constraints linking to # some other armature bones, for example @@ -319,20 +322,21 @@ def __get_blender_actions(blender_object: bpy.types.Object, action_on_type[strip.action.name] = "SHAPEKEY" # If there are only 1 armature, include all animations, even if not in NLA - if blender_object.type == "ARMATURE": - if len(export_settings['vtree'].get_all_node_of_type(VExportNode.ARMATURE)) == 1: - # Keep all actions on objects (no Shapekey animation) - for act in [a for a in bpy.data.actions if a.id_root == "OBJECT"]: - # We need to check this is an armature action - # Checking that at least 1 bone is animated - if not __is_armature_action(act): - continue - # Check if this action is already taken into account - if act.name in blender_tracks.keys(): - continue - blender_actions.append(act) - blender_tracks[act.name] = None - action_on_type[act.name] = "OBJECT" + if export_settings['gltf_export_anim_single_armature'] is True: + if blender_object.type == "ARMATURE": + if len(export_settings['vtree'].get_all_node_of_type(VExportNode.ARMATURE)) == 1: + # Keep all actions on objects (no Shapekey animation) + for act in [a for a in bpy.data.actions if a.id_root == "OBJECT"]: + # We need to check this is an armature action + # Checking that at least 1 bone is animated + if not __is_armature_action(act): + continue + # Check if this action is already taken into account + if act.name in blender_tracks.keys(): + continue + blender_actions.append(act) + blender_tracks[act.name] = None + action_on_type[act.name] = "OBJECT" export_user_extensions('gather_actions_hook', export_settings, blender_object, blender_actions, blender_tracks, action_on_type) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py index b0e538c8..9da5cc65 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_drivers.py @@ -13,7 +13,7 @@ def get_sk_drivers(blender_armature_uuid, export_settings): drivers = [] # Take into account skinned mesh, and mesh parented to a bone of the armature - children_list = export_settings['vtree'].nodes[blender_armature_uuid].children + children_list = export_settings['vtree'].nodes[blender_armature_uuid].children.copy() for bone in export_settings['vtree'].get_all_bones(blender_armature_uuid): children_list.extend(export_settings['vtree'].nodes[bone].children) @@ -74,7 +74,8 @@ def get_sk_drivers(blender_armature_uuid, export_settings): else: all_sorted_channels.append(existing_idx[i]) - if len(all_sorted_channels) > 0: + # Checks there are some driver on SK, and that there is not only invalid drivers + if len(all_sorted_channels) > 0 and not all([i is None for i in all_sorted_channels]): drivers.append((child_uuid, tuple(all_sorted_channels))) return tuple(drivers) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py index 0dfce9f9..81e79a50 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -11,7 +11,7 @@ from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.exp import gltf2_io_image_data from io_scene_gltf2.io.com import gltf2_io_debug -from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, ExportImage, FillImage +from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, ExportImage, FillImage, StoreImage, StoreData from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions @@ -21,26 +21,31 @@ def gather_image( blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], export_settings): if not __filter_image(blender_shader_sockets, export_settings): - return None + return None, None image_data = __get_image_data(blender_shader_sockets, export_settings) if image_data.empty(): # The export image has no data - return None + return None, None mime_type = __gather_mime_type(blender_shader_sockets, image_data, export_settings) name = __gather_name(image_data, export_settings) + factor = None + if image_data.original is None: - uri = __gather_uri(image_data, mime_type, name, export_settings) + uri, factor_uri = __gather_uri(image_data, mime_type, name, export_settings) else: # Retrieve URI relative to exported glTF files uri = __gather_original_uri(image_data.original.filepath, export_settings) # In case we can't retrieve image (for example packed images, with original moved) # We don't create invalid image without uri + factor_uri = None if uri is None: return None - buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + buffer_view, factor_buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + + factor = factor_uri if uri is not None else factor_buffer_view image = __make_image( buffer_view, @@ -54,7 +59,7 @@ def gather_image( export_user_extensions('gather_image_hook', export_settings, image, blender_shader_sockets) - return image + return image, factor def __gather_original_uri(original_uri, export_settings): @@ -98,8 +103,9 @@ def __filter_image(sockets, export_settings): @cached def __gather_buffer_view(image_data, mime_type, name, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] != 'GLTF_SEPARATE': - return gltf2_io_binary_data.BinaryData(data=image_data.encode(mime_type)) - return None + data, factor = image_data.encode(mime_type) + return gltf2_io_binary_data.BinaryData(data=data), factor + return None, None def __gather_extensions(sockets, export_settings): @@ -165,13 +171,14 @@ def __gather_name(export_image, export_settings): def __gather_uri(image_data, mime_type, name, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] == 'GLTF_SEPARATE': # as usual we just store the data in place instead of already resolving the references + data, factor = image_data.encode(mime_type=mime_type) return gltf2_io_image_data.ImageData( - data=image_data.encode(mime_type=mime_type), + data=data, mime_type=mime_type, name=name - ) + ), factor - return None + return None, None def __get_image_data(sockets, export_settings) -> ExportImage: @@ -179,6 +186,18 @@ def __get_image_data(sockets, export_settings) -> ExportImage: # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary # resources. results = [__get_tex_from_socket(socket, export_settings) for socket in sockets] + + # Check if we need a simple mapping or more complex calculation + if any([socket.name == "Specular" and socket.node.type == "BSDF_PRINCIPLED" for socket in sockets]): + return __get_image_data_specular(sockets, results, export_settings) + else: + return __get_image_data_mapping(sockets, results, export_settings) + +def __get_image_data_mapping(sockets, results, export_settings) -> ExportImage: + """ + Simple mapping + Will fit for most of exported textures : RoughnessMetallic, Basecolor, normal, ... + """ composed_image = ExportImage() for result, socket in zip(results, sockets): # Assume that user know what he does, and that channels/images are already combined correctly for pbr @@ -217,6 +236,12 @@ def __get_image_data(sockets, export_settings) -> ExportImage: dst_chan = Channel.R elif socket.name == 'Clearcoat Roughness': dst_chan = Channel.G + elif socket.name == 'Thickness': # For KHR_materials_volume + dst_chan = Channel.G + elif socket.name == "Specular": # For original KHR_material_specular + dst_chan = Channel.A + elif socket.name == "Sigma": # For KHR_materials_sheen + dst_chan = Channel.A if dst_chan is not None: composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) @@ -243,6 +268,54 @@ def __get_image_data(sockets, export_settings) -> ExportImage: return composed_image +def __get_image_data_specular(sockets, results, export_settings) -> ExportImage: + """ + calculating Specular Texture, settings needed data + """ + from io_scene_gltf2.blender.exp.gltf2_blender_texture_specular import specular_calculation + composed_image = ExportImage() + composed_image.set_calc(specular_calculation) + + composed_image.store_data("ior", sockets[4].default_value, type="Data") + + results = [__get_tex_from_socket(socket, export_settings) for socket in sockets[:-1]] #Do not retrieve IOR --> No texture allowed + + mapping = { + 0: "specular", + 1: "specular_tint", + 2: "base_color", + 3: "transmission" + } + + for idx, result in enumerate(results): + if __get_tex_from_socket(sockets[idx], export_settings): + + composed_image.store_data(mapping[idx], result.shader_node.image, type="Image") + + # rudimentarily try follow the node tree to find the correct image data. + src_chan = None if idx == 2 else Channel.R + for elem in result.path: + if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateColor): + src_chan = { + 'Red': Channel.R, + 'Green': Channel.G, + 'Blue': Channel.B, + }[elem.from_socket.name] + if elem.from_socket.name == 'Alpha': + src_chan = Channel.A + # For base_color, keep all channels, as this is a Vec, not scalar + if idx != 2: + composed_image.store_data(mapping[idx] + "_channel", src_chan, type="Data") + else: + if src_chan is not None: + composed_image.store_data(mapping[idx] + "_channel", src_chan, type="Data") + + else: + composed_image.store_data(mapping[idx], sockets[idx].default_value, type="Data") + + return composed_image + +# TODOExt deduplicate @cached def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket, export_settings): result = gltf2_blender_search_node_tree.from_socket( diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py index 56c3acff..c0d17fd3 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -1,21 +1,27 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright 2018-2021 The glTF-Blender-IO authors. +# Copyright 2018-2022 The glTF-Blender-IO authors. from copy import deepcopy import bpy from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key from io_scene_gltf2.io.com import gltf2_io -from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_unlit from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_export_keys -from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree - from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness -from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_unlit from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.blender.exp import gltf2_blender_get from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_volume import export_volume +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_emission import export_emission_factor, \ + export_emission_texture, export_emission_strength_extension +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_sheen import export_sheen +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_specular import export_specular +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_transmission import export_transmission +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_clearcoat import export_clearcoat +from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials_ior import export_ior +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension @cached def get_material_cache_key(blender_material, active_uvmap_index, export_settings): @@ -39,24 +45,31 @@ def gather_material(blender_material, active_uvmap_index, export_settings): if not __filter_material(blender_material, export_settings): return None - mat_unlit = __gather_material_unlit(blender_material, active_uvmap_index, export_settings) + mat_unlit = __export_unlit(blender_material, active_uvmap_index, export_settings) if mat_unlit is not None: export_user_extensions('gather_material_hook', export_settings, mat_unlit, blender_material) return mat_unlit orm_texture = __gather_orm_texture(blender_material, export_settings) + emissive_factor = __gather_emissive_factor(blender_material, export_settings) emissive_texture, uvmap_actives_emissive_texture = __gather_emissive_texture(blender_material, export_settings) - extensions, uvmap_actives_extensions = __gather_extensions(blender_material, export_settings) + extensions, uvmap_actives_extensions = __gather_extensions(blender_material, emissive_factor, export_settings) normal_texture, uvmap_actives_normal_texture = __gather_normal_texture(blender_material, export_settings) occlusion_texture, uvmap_actives_occlusion_texture = __gather_occlusion_texture(blender_material, orm_texture, export_settings) pbr_metallic_roughness, uvmap_actives_pbr_metallic_roughness = __gather_pbr_metallic_roughness(blender_material, orm_texture, export_settings) + if any([i>1.0 for i in emissive_factor or []]) is True: + # Strength is set on extension + emission_strength = max(emissive_factor) + emissive_factor = [f / emission_strength for f in emissive_factor] + + base_material = gltf2_io.Material( alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings), alpha_mode=__gather_alpha_mode(blender_material, export_settings), double_sided=__gather_double_sided(blender_material, export_settings), - emissive_factor=__gather_emissive_factor(blender_material, export_settings), + emissive_factor=emissive_factor, emissive_texture=emissive_texture, extensions=extensions, extras=__gather_extras(blender_material, export_settings), @@ -106,6 +119,14 @@ def gather_material(blender_material, active_uvmap_index, export_settings): material.extensions["KHR_materials_clearcoat"].extension['clearcoatNormalTexture'].tex_coord = active_uvmap_index elif tex == "transmissionTexture": #TODO not tested yet material.extensions["KHR_materials_transmission"].extension['transmissionTexture'].tex_coord = active_uvmap_index + elif tex == "specularTexture": + material.extensions["KHR_materials_specular"].extension['specularTexture'].tex_coord = active_uvmap_index + elif tex == "specularColorTexture": + material.extensions["KHR_materials_specular"].extension['specularColorTexture'].tex_coord = active_uvmap_index + elif tex == "sheenColorTexture": + material.extensions["KHR_materials_sheen"].extension['sheenColorTexture'].tex_coord = active_uvmap_index + elif tex == "sheenRoughnessTexture": + material.extensions["KHR_materials_sheen"].extension['sheenRoughnessTexture'].tex_coord = active_uvmap_index # If material is not using active UVMap, we need to return the same material, # Even if multiples meshes are using different active UVMap @@ -188,72 +209,61 @@ def __gather_double_sided(blender_material, export_settings): def __gather_emissive_factor(blender_material, export_settings): - emissive_socket = gltf2_blender_get.get_socket(blender_material, "Emissive") - if emissive_socket is None: - emissive_socket = gltf2_blender_get.get_socket_old(blender_material, "EmissiveFactor") - if isinstance(emissive_socket, bpy.types.NodeSocket): - if export_settings['gltf_image_format'] != "NONE": - factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB') - else: - factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB') - - if factor is None and emissive_socket.is_linked: - # In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected, - # we have to manually set it to all ones. - factor = [1.0, 1.0, 1.0] - - if factor is None: factor = [0.0, 0.0, 0.0] - - # Handle Emission Strength - strength_socket = None - if emissive_socket.node.type == 'EMISSION': - strength_socket = emissive_socket.node.inputs['Strength'] - elif 'Emission Strength' in emissive_socket.node.inputs: - strength_socket = emissive_socket.node.inputs['Emission Strength'] - strength = ( - gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE') - if strength_socket is not None - else None - ) - if strength is not None: - factor = [f * strength for f in factor] - - # Clamp to range [0,1] - factor = [min(1.0, f) for f in factor] - - if factor == [0, 0, 0]: factor = None - - return factor - - return None - + return export_emission_factor(blender_material, export_settings) def __gather_emissive_texture(blender_material, export_settings): - emissive = gltf2_blender_get.get_socket(blender_material, "Emissive") - if emissive is None: - emissive = gltf2_blender_get.get_socket_old(blender_material, "Emissive") - emissive_texture, use_actives_uvmap_emissive = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), export_settings) - return emissive_texture, ["emissiveTexture"] if use_actives_uvmap_emissive else None + return export_emission_texture(blender_material, export_settings) -def __gather_extensions(blender_material, export_settings): +def __gather_extensions(blender_material, emissive_factor, export_settings): extensions = {} # KHR_materials_clearcoat actives_uvmaps = [] - clearcoat_extension, use_actives_uvmap_clearcoat = __gather_clearcoat_extension(blender_material, export_settings) + clearcoat_extension, use_actives_uvmap_clearcoat = export_clearcoat(blender_material, export_settings) if clearcoat_extension: extensions["KHR_materials_clearcoat"] = clearcoat_extension actives_uvmaps.extend(use_actives_uvmap_clearcoat) # KHR_materials_transmission - transmission_extension, use_actives_uvmap_transmission = __gather_transmission_extension(blender_material, export_settings) + transmission_extension, use_actives_uvmap_transmission = export_transmission(blender_material, export_settings) if transmission_extension: extensions["KHR_materials_transmission"] = transmission_extension actives_uvmaps.extend(use_actives_uvmap_transmission) + # KHR_materials_emissive_strength + if any([i>1.0 for i in emissive_factor or []]): + emissive_strength_extension = export_emission_strength_extension(emissive_factor, export_settings) + if emissive_strength_extension: + extensions["KHR_materials_emissive_strength"] = emissive_strength_extension + + # KHR_materials_volume + + volume_extension, use_actives_uvmap_volume_thickness = export_volume(blender_material, export_settings) + if volume_extension: + extensions["KHR_materials_volume"] = volume_extension + actives_uvmaps.extend(use_actives_uvmap_volume_thickness) + + # KHR_materials_specular + specular_extension, use_actives_uvmap_specular = export_specular(blender_material, export_settings) + if specular_extension: + extensions["KHR_materials_specular"] = specular_extension + actives_uvmaps.extend(use_actives_uvmap_specular) + + # KHR_materials_sheen + sheen_extension, use_actives_uvmap_sheen = export_sheen(blender_material, export_settings) + if sheen_extension: + extensions["KHR_materials_sheen"] = sheen_extension + actives_uvmaps.extend(use_actives_uvmap_sheen) + + # KHR_materials_ior + # Keep this extension at the end, because we export it only if some others are exported + ior_extension = export_ior(blender_material, extensions, export_settings) + if ior_extension: + extensions["KHR_materials_ior"] = ior_extension + return extensions, actives_uvmaps if extensions else None @@ -271,7 +281,7 @@ def __gather_normal_texture(blender_material, export_settings): normal = gltf2_blender_get.get_socket(blender_material, "Normal") if normal is None: normal = gltf2_blender_get.get_socket_old(blender_material, "Normal") - normal_texture, use_active_uvmap_normal = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( + normal_texture, use_active_uvmap_normal, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( normal, (normal,), export_settings) @@ -283,20 +293,20 @@ def __gather_orm_texture(blender_material, export_settings): # If not fully shared, return None, so the images will be cached and processed separately. occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion") - if occlusion is None or not __has_image_node_from_socket(occlusion): + if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion): occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion") - if occlusion is None or not __has_image_node_from_socket(occlusion): + if occlusion is None or not gltf2_blender_get.has_image_node_from_socket(occlusion): return None metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic") roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness") - hasMetal = metallic_socket is not None and __has_image_node_from_socket(metallic_socket) - hasRough = roughness_socket is not None and __has_image_node_from_socket(roughness_socket) + hasMetal = metallic_socket is not None and gltf2_blender_get.has_image_node_from_socket(metallic_socket) + hasRough = roughness_socket is not None and gltf2_blender_get.has_image_node_from_socket(roughness_socket) if not hasMetal and not hasRough: metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness") - if metallic_roughness is None or not __has_image_node_from_socket(metallic_roughness): + if metallic_roughness is None or not gltf2_blender_get.has_image_node_from_socket(metallic_roughness): return None result = (occlusion, metallic_roughness) elif not hasMetal: @@ -313,7 +323,7 @@ def __gather_orm_texture(blender_material, export_settings): return None # Double-check this will past the filter in texture_info - info, info_use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info(result[0], result, export_settings) + info, info_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info(result[0], result, export_settings) if info is None: return None @@ -323,7 +333,7 @@ def __gather_occlusion_texture(blender_material, orm_texture, export_settings): occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion") if occlusion is None: occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion") - occlusion_texture, use_active_uvmap_occlusion = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class( + occlusion_texture, use_active_uvmap_occlusion, _ = gltf2_blender_gather_texture_info.gather_material_occlusion_texture_info_class( occlusion, orm_texture or (occlusion,), export_settings) @@ -336,129 +346,7 @@ def __gather_pbr_metallic_roughness(blender_material, orm_texture, export_settin orm_texture, export_settings) -def __has_image_node_from_socket(socket): - result = gltf2_blender_search_node_tree.from_socket( - socket, - gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) - if not result: - return False - return True - -def __gather_clearcoat_extension(blender_material, export_settings): - clearcoat_enabled = False - has_clearcoat_texture = False - has_clearcoat_roughness_texture = False - - clearcoat_extension = {} - clearcoat_roughness_slots = () - - clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat') - clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Roughness') - clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Normal') - - if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: - clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value - clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0 - elif __has_image_node_from_socket(clearcoat_socket): - fac = gltf2_blender_get.get_factor_from_socket(clearcoat_socket, kind='VALUE') - # default value in glTF is 0.0, but if there is a texture without factor, use 1 - clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0 - has_clearcoat_texture = True - clearcoat_enabled = True - - if not clearcoat_enabled: - return None, None - - if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked: - clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value - elif __has_image_node_from_socket(clearcoat_roughness_socket): - fac = gltf2_blender_get.get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE') - # default value in glTF is 0.0, but if there is a texture without factor, use 1 - clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0 - has_clearcoat_roughness_texture = True - - # Pack clearcoat (R) and clearcoatRoughness (G) channels. - if has_clearcoat_texture and has_clearcoat_roughness_texture: - clearcoat_roughness_slots = (clearcoat_socket, clearcoat_roughness_socket,) - elif has_clearcoat_texture: - clearcoat_roughness_slots = (clearcoat_socket,) - elif has_clearcoat_roughness_texture: - clearcoat_roughness_slots = (clearcoat_roughness_socket,) - - use_actives_uvmaps = [] - - if len(clearcoat_roughness_slots) > 0: - if has_clearcoat_texture: - clearcoat_texture, clearcoat_texture_use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( - clearcoat_socket, - clearcoat_roughness_slots, - export_settings, - ) - clearcoat_extension['clearcoatTexture'] = clearcoat_texture - if clearcoat_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatTexture") - if has_clearcoat_roughness_texture: - clearcoat_roughness_texture, clearcoat_roughness_texture_use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( - clearcoat_roughness_socket, - clearcoat_roughness_slots, - export_settings, - ) - clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture - if clearcoat_roughness_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatRoughnessTexture") - if __has_image_node_from_socket(clearcoat_normal_socket): - clearcoat_normal_texture, clearcoat_normal_texture_use_active_uvmap = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( - clearcoat_normal_socket, - (clearcoat_normal_socket,), - export_settings - ) - clearcoat_extension['clearcoatNormalTexture'] = clearcoat_normal_texture - if clearcoat_normal_texture_use_active_uvmap: - use_actives_uvmaps.append("clearcoatNormalTexture") - - return Extension('KHR_materials_clearcoat', clearcoat_extension, False), use_actives_uvmaps - -def __gather_transmission_extension(blender_material, export_settings): - transmission_enabled = False - has_transmission_texture = False - - transmission_extension = {} - transmission_slots = () - - transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') - - if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: - transmission_extension['transmissionFactor'] = transmission_socket.default_value - transmission_enabled = transmission_extension['transmissionFactor'] > 0 - elif __has_image_node_from_socket(transmission_socket): - transmission_extension['transmissionFactor'] = 1.0 - has_transmission_texture = True - transmission_enabled = True - - if not transmission_enabled: - return None, None - - # Pack transmission channel (R). - if has_transmission_texture: - transmission_slots = (transmission_socket,) - - use_actives_uvmaps = [] - - if len(transmission_slots) > 0: - combined_texture, use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( - transmission_socket, - transmission_slots, - export_settings, - ) - if has_transmission_texture: - transmission_extension['transmissionTexture'] = combined_texture - if use_active_uvmap: - use_actives_uvmaps.append("transmissionTexture") - - return Extension('KHR_materials_transmission', transmission_extension, False), use_actives_uvmaps - - -def __gather_material_unlit(blender_material, active_uvmap_index, export_settings): +def __export_unlit(blender_material, active_uvmap_index, export_settings): gltf2_unlit = gltf2_blender_gather_materials_unlit info = gltf2_unlit.detect_shadeless_material(blender_material, export_settings) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py new file mode 100644 index 00000000..65c164b4 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_clearcoat.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_clearcoat(blender_material, export_settings): + clearcoat_enabled = False + has_clearcoat_texture = False + has_clearcoat_roughness_texture = False + + clearcoat_extension = {} + clearcoat_roughness_slots = () + + clearcoat_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat') + clearcoat_roughness_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Roughness') + clearcoat_normal_socket = gltf2_blender_get.get_socket(blender_material, 'Clearcoat Normal') + + if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: + clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value + clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0 + elif gltf2_blender_get.has_image_node_from_socket(clearcoat_socket): + fac = gltf2_blender_get.get_factor_from_socket(clearcoat_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + clearcoat_extension['clearcoatFactor'] = fac if fac != None else 1.0 + has_clearcoat_texture = True + clearcoat_enabled = True + + if not clearcoat_enabled: + return None, None + + if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked: + clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value + elif gltf2_blender_get.has_image_node_from_socket(clearcoat_roughness_socket): + fac = gltf2_blender_get.get_factor_from_socket(clearcoat_roughness_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + clearcoat_extension['clearcoatRoughnessFactor'] = fac if fac != None else 1.0 + has_clearcoat_roughness_texture = True + + # Pack clearcoat (R) and clearcoatRoughness (G) channels. + if has_clearcoat_texture and has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_socket, clearcoat_roughness_socket,) + elif has_clearcoat_texture: + clearcoat_roughness_slots = (clearcoat_socket,) + elif has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_roughness_socket,) + + use_actives_uvmaps = [] + + if len(clearcoat_roughness_slots) > 0: + if has_clearcoat_texture: + clearcoat_texture, clearcoat_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + clearcoat_socket, + clearcoat_roughness_slots, + export_settings, + ) + clearcoat_extension['clearcoatTexture'] = clearcoat_texture + if clearcoat_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatTexture") + if has_clearcoat_roughness_texture: + clearcoat_roughness_texture, clearcoat_roughness_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + clearcoat_roughness_socket, + clearcoat_roughness_slots, + export_settings, + ) + clearcoat_extension['clearcoatRoughnessTexture'] = clearcoat_roughness_texture + if clearcoat_roughness_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatRoughnessTexture") + if gltf2_blender_get.has_image_node_from_socket(clearcoat_normal_socket): + clearcoat_normal_texture, clearcoat_normal_texture_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_material_normal_texture_info_class( + clearcoat_normal_socket, + (clearcoat_normal_socket,), + export_settings + ) + clearcoat_extension['clearcoatNormalTexture'] = clearcoat_normal_texture + if clearcoat_normal_texture_use_active_uvmap: + use_actives_uvmaps.append("clearcoatNormalTexture") + + return Extension('KHR_materials_clearcoat', clearcoat_extension, False), use_actives_uvmaps
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py new file mode 100644 index 00000000..562fc19d --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_emission.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_emission_factor(blender_material, export_settings): + emissive_socket = gltf2_blender_get.get_socket(blender_material, "Emissive") + if emissive_socket is None: + emissive_socket = gltf2_blender_get.get_socket_old(blender_material, "EmissiveFactor") + if isinstance(emissive_socket, bpy.types.NodeSocket): + if export_settings['gltf_image_format'] != "NONE": + factor = gltf2_blender_get.get_factor_from_socket(emissive_socket, kind='RGB') + else: + factor = gltf2_blender_get.get_const_from_default_value_socket(emissive_socket, kind='RGB') + + if factor is None and emissive_socket.is_linked: + # In glTF, the default emissiveFactor is all zeros, so if an emission texture is connected, + # we have to manually set it to all ones. + factor = [1.0, 1.0, 1.0] + + if factor is None: factor = [0.0, 0.0, 0.0] + + # Handle Emission Strength + strength_socket = None + if emissive_socket.node.type == 'EMISSION': + strength_socket = emissive_socket.node.inputs['Strength'] + elif 'Emission Strength' in emissive_socket.node.inputs: + strength_socket = emissive_socket.node.inputs['Emission Strength'] + strength = ( + gltf2_blender_get.get_const_from_socket(strength_socket, kind='VALUE') + if strength_socket is not None + else None + ) + if strength is not None: + factor = [f * strength for f in factor] + + # Clamp to range [0,1] + # Official glTF clamp to range [0,1] + # If we are outside, we need to use extension KHR_materials_emissive_strength + + if factor == [0, 0, 0]: factor = None + + return factor + + return None + +def export_emission_texture(blender_material, export_settings): + emissive = gltf2_blender_get.get_socket(blender_material, "Emissive") + if emissive is None: + emissive = gltf2_blender_get.get_socket_old(blender_material, "Emissive") + emissive_texture, use_actives_uvmap_emissive, _ = gltf2_blender_gather_texture_info.gather_texture_info(emissive, (emissive,), export_settings) + return emissive_texture, ["emissiveTexture"] if use_actives_uvmap_emissive else None + +def export_emission_strength_extension(emissive_factor, export_settings): + emissive_strength_extension = {} + emissive_strength_extension['emissiveStrength'] = max(emissive_factor) + + return Extension('KHR_materials_emissive_strength', emissive_strength_extension, False)
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py new file mode 100644 index 00000000..fc219c01 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_ior.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.io.com.gltf2_io_constants import GLTF_IOR + +def export_ior(blender_material, extensions, export_settings): + ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR') + + if not ior_socket: + return None + + # We don't manage case where socket is linked, always check default value + if ior_socket.is_linked: + # TODOExt: add warning? + return None + + if ior_socket.default_value == GLTF_IOR: + return None + + # Export only if the following extensions are exported: + need_to_export_ior = [ + 'KHR_materials_transmission', + 'KHR_materials_volume', + 'KHR_materials_specular' + ] + + if not any([e in extensions.keys() for e in need_to_export_ior]): + return None + + ior_extension = {} + ior_extension['ior'] = ior_socket.default_value + + return Extension('KHR_materials_ior', ior_extension, False)
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py index 0b40ffd6..a5929c05 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py @@ -15,8 +15,8 @@ def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export if not __filter_pbr_material(blender_material, export_settings): return None, None - base_color_texture, use_active_uvmap_base_color_texture = __gather_base_color_texture(blender_material, export_settings) - metallic_roughness_texture, use_active_uvmap_metallic_roughness_texture = __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings) + base_color_texture, use_active_uvmap_base_color_texture, _ = __gather_base_color_texture(blender_material, export_settings) + metallic_roughness_texture, use_active_uvmap_metallic_roughness_texture, _ = __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings) material = gltf2_io.MaterialPBRMetallicRoughness( base_color_factor=__gather_base_color_factor(blender_material, export_settings), @@ -92,7 +92,7 @@ def __gather_base_color_texture(blender_material, export_settings): if socket is not None and __has_image_node_from_socket(socket) ) if not inputs: - return None, None + return None, None, None return gltf2_blender_gather_texture_info.gather_texture_info(inputs[0], inputs, export_settings) @@ -128,7 +128,7 @@ def __gather_metallic_roughness_texture(blender_material, orm_texture, export_se if not hasMetal and not hasRough: metallic_roughness = gltf2_blender_get.get_socket_old(blender_material, "MetallicRoughness") if metallic_roughness is None or not __has_image_node_from_socket(metallic_roughness): - return None, None + return None, None, None texture_input = (metallic_roughness,) elif not hasMetal: texture_input = (roughness_socket,) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py new file mode 100644 index 00000000..03625ecb --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_sheen.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + +def export_sheen(blender_material, export_settings): + sheen_extension = {} + + sheenColor_socket = gltf2_blender_get.get_socket(blender_material, "sheenColor") + sheenRoughness_socket = gltf2_blender_get.get_socket(blender_material, "sheenRoughness") + + if sheenColor_socket is None or sheenRoughness_socket is None: + return None, None + + sheenColor_non_linked = isinstance(sheenColor_socket, bpy.types.NodeSocket) and not sheenColor_socket.is_linked + sheenRoughness_non_linked = isinstance(sheenRoughness_socket, bpy.types.NodeSocket) and not sheenRoughness_socket.is_linked + + + use_actives_uvmaps = [] + + if sheenColor_non_linked is True: + color = sheenColor_socket.default_value[:3] + if color != (0.0, 0.0, 0.0): + sheen_extension['sheenColorFactor'] = color + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(sheenColor_socket, kind='RGB') + if fac is not None and fac != [0.0, 0.0, 0.0]: + sheen_extension['sheenColorFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(sheenColor_socket): + original_sheenColor_texture, original_sheenColor_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + sheenColor_socket, + (sheenColor_socket,), + export_settings, + ) + sheen_extension['sheenColorTexture'] = original_sheenColor_texture + if original_sheenColor_use_active_uvmap: + use_actives_uvmaps.append("sheenColorTexture") + + + if sheenRoughness_non_linked is True: + fac = sheenRoughness_socket.default_value + if fac != 0.0: + sheen_extension['sheenRoughnessFactor'] = fac + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(sheenRoughness_socket, kind='VALUE') + if fac is not None and fac != 0.0: + sheen_extension['sheenRoughnessFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(sheenRoughness_socket): + original_sheenRoughness_texture, original_sheenRoughness_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + sheenRoughness_socket, + (sheenRoughness_socket,), + export_settings, + ) + sheen_extension['sheenRoughnessTexture'] = original_sheenRoughness_texture + if original_sheenRoughness_use_active_uvmap: + use_actives_uvmaps.append("sheenRoughnessTexture") + + return Extension('KHR_materials_sheen', sheen_extension, False), use_actives_uvmaps diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py new file mode 100644 index 00000000..22414b13 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_specular.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.io.com.gltf2_io_constants import GLTF_IOR +from io_scene_gltf2.blender.com.gltf2_blender_default import BLENDER_SPECULAR, BLENDER_SPECULAR_TINT +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + + +def export_original_specular(blender_material, export_settings): + specular_extension = {} + + original_specular_socket = gltf2_blender_get.get_socket_old(blender_material, 'Specular') + original_specularcolor_socket = gltf2_blender_get.get_socket_old(blender_material, 'Specular Color') + + if original_specular_socket is None or original_specularcolor_socket is None: + return None, None + + specular_non_linked = isinstance(original_specular_socket, bpy.types.NodeSocket) and not original_specular_socket.is_linked + specularcolor_non_linked = isinstance(original_specularcolor_socket, bpy.types.NodeSocket) and not original_specularcolor_socket.is_linked + + + use_actives_uvmaps = [] + + if specular_non_linked is True: + fac = original_specular_socket.default_value + if fac != 1.0: + specular_extension['specularFactor'] = fac + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(original_specular_socket, kind='VALUE') + if fac is not None and fac != 1.0: + specular_extension['specularFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(original_specular_socket): + original_specular_texture, original_specular_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + original_specular_socket, + (original_specular_socket,), + export_settings, + ) + specular_extension['specularTexture'] = original_specular_texture + if original_specular_use_active_uvmap: + use_actives_uvmaps.append("specularTexture") + + + if specularcolor_non_linked is True: + color = original_specularcolor_socket.default_value[:3] + if color != [1.0, 1.0, 1.0]: + specular_extension['specularColorFactor'] = color + else: + # Factor + fac = gltf2_blender_get.get_factor_from_socket(original_specularcolor_socket, kind='RGB') + if fac is not None and fac != [1.0, 1.0, 1.0]: + specular_extension['specularColorFactor'] = fac + + # Texture + if gltf2_blender_get.has_image_node_from_socket(original_specularcolor_socket): + original_specularcolor_texture, original_specularcolor_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + original_specularcolor_socket, + (original_specularcolor_socket,), + export_settings, + ) + specular_extension['specularColorTexture'] = original_specularcolor_texture + if original_specularcolor_use_active_uvmap: + use_actives_uvmaps.append("specularColorTexture") + + return Extension('KHR_materials_specular', specular_extension, False), use_actives_uvmaps + +def export_specular(blender_material, export_settings): + + if export_settings['gltf_original_specular'] is True: + return export_original_specular(blender_material, export_settings) + + specular_extension = {} + specular_ext_enabled = False + + specular_socket = gltf2_blender_get.get_socket(blender_material, 'Specular') + specular_tint_socket = gltf2_blender_get.get_socket(blender_material, 'Specular Tint') + base_color_socket = gltf2_blender_get.get_socket(blender_material, 'Base Color') + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR') + + if base_color_socket is None: + return None, None + + # TODOExt replace by __has_image_node_from_socket calls + specular_not_linked = isinstance(specular_socket, bpy.types.NodeSocket) and not specular_socket.is_linked + specular_tint_not_linked = isinstance(specular_tint_socket, bpy.types.NodeSocket) and not specular_tint_socket.is_linked + base_color_not_linked = isinstance(base_color_socket, bpy.types.NodeSocket) and not base_color_socket.is_linked + transmission_not_linked = isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked + ior_not_linked = isinstance(ior_socket, bpy.types.NodeSocket) and not ior_socket.is_linked + + specular = specular_socket.default_value if specular_not_linked else None + specular_tint = specular_tint_socket.default_value if specular_tint_not_linked else None + transmission = transmission_socket.default_value if transmission_not_linked else None + ior = ior_socket.default_value if ior_not_linked else GLTF_IOR # textures not supported #TODOExt add warning? + base_color = base_color_socket.default_value[0:3] + + no_texture = (transmission_not_linked and specular_not_linked and specular_tint_not_linked and + (specular_tint == 0.0 or (specular_tint != 0.0 and base_color_not_linked))) + + use_actives_uvmaps = [] + + if no_texture: + if specular != BLENDER_SPECULAR or specular_tint != BLENDER_SPECULAR_TINT: + import numpy as np + # See https://gist.github.com/proog128/d627c692a6bbe584d66789a5a6437a33 + specular_ext_enabled = True + + def normalize(c): + luminance = lambda c: 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2] + assert(len(c) == 3) + l = luminance(c) + if l == 0: + return np.array(c) + return np.array([c[0] / l, c[1] / l, c[2] / l]) + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + tint_strength = (1 - specular_tint) + normalize(base_color) * specular_tint + specular_color = (1 - transmission) * (1 / f0_from_ior) * 0.08 * specular * tint_strength + transmission * tint_strength + specular_extension['specularColorFactor'] = list(specular_color) + else: + if specular_not_linked and specular == BLENDER_SPECULAR and specular_tint_not_linked and specular_tint == BLENDER_SPECULAR_TINT: + return None, None + + # Trying to identify cases where exporting a texture will not be needed + if specular_not_linked and transmission_not_linked and \ + specular == 0.0 and transmission == 0.0: + + specular_extension['specularColorFactor'] = [0.0, 0.0, 0.0] + return specular_extension, [] + + + # There will be a texture, with a complex calculation (no direct channel mapping) + sockets = (specular_socket, specular_tint_socket, base_color_socket, transmission_socket, ior_socket) + # Set primary socket having a texture + primary_socket = specular_socket + if specular_not_linked: + primary_socket = specular_tint_socket + if specular_tint_not_linked: + primary_socket = base_color_socket + if base_color_not_linked: + primary_socket = transmission_socket + + specularColorTexture, use_active_uvmap, specularColorFactor = gltf2_blender_gather_texture_info.gather_texture_info( + primary_socket, + sockets, + export_settings, + filter_type='ANY') + if specularColorTexture is None: + return None, None + if use_active_uvmap: + use_actives_uvmaps.append("specularColorTexture") + + specular_ext_enabled = True + specular_extension['specularColorTexture'] = specularColorTexture + + + if specularColorFactor is not None: + specular_extension['specularColorFactor'] = specularColorFactor + + + specular_extension = Extension('KHR_materials_specular', specular_extension, False) if specular_ext_enabled else None + return specular_extension, use_actives_uvmaps
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py new file mode 100644 index 00000000..fdc5d8c7 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_transmission.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + +def export_transmission(blender_material, export_settings): + transmission_enabled = False + has_transmission_texture = False + + transmission_extension = {} + transmission_slots = () + + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + + if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: + transmission_extension['transmissionFactor'] = transmission_socket.default_value + transmission_enabled = transmission_extension['transmissionFactor'] > 0 + elif gltf2_blender_get.has_image_node_from_socket(transmission_socket): + fac = gltf2_blender_get.get_factor_from_socket(transmission_socket, kind='VALUE') + transmission_extension['transmissionFactor'] = fac if fac is not None else 1.0 + has_transmission_texture = True + transmission_enabled = True + + if not transmission_enabled: + return None, None + + # Pack transmission channel (R). + if has_transmission_texture: + transmission_slots = (transmission_socket,) + + use_actives_uvmaps = [] + + if len(transmission_slots) > 0: + combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + transmission_socket, + transmission_slots, + export_settings, + ) + if has_transmission_texture: + transmission_extension['transmissionTexture'] = combined_texture + if use_active_uvmap: + use_actives_uvmaps.append("transmissionTexture") + + return Extension('KHR_materials_transmission', transmission_extension, False), use_actives_uvmaps
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py index e104b7f1..b501f98f 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_unlit.py @@ -1,9 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright 2018-2021 The glTF-Blender-IO authors. +# Copyright 2018-2022 The glTF-Blender-IO authors. from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info from io_scene_gltf2.blender.exp import gltf2_blender_get - +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension def detect_shadeless_material(blender_material, export_settings): """Detect if this material is "shadeless" ie. should be exported @@ -127,7 +127,7 @@ def gather_base_color_texture(info, export_settings): # because gather_image determines how to pack images based on the # names of sockets, and the names are hard-coded to a Principled # style graph. - unlit_texture, unlit_use_active_uvmap = gltf2_blender_gather_texture_info.gather_texture_info( + unlit_texture, unlit_use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( sockets[0], sockets, export_settings, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py new file mode 100644 index 00000000..4c452e6a --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_variants.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from typing import Dict, Any +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io_variants + + +@cached +def gather_variant(variant_idx, export_settings) -> Dict[str, Any]: + + variant = gltf2_io_variants.Variant( + name=bpy.data.scenes[0].gltf2_KHR_materials_variants_variants[variant_idx].name, + extensions=None, + extras=None + ) + return variant.to_dict() diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py new file mode 100644 index 00000000..8a69e3f6 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_volume.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info + + +def export_volume(blender_material, export_settings): + # Implementation based on https://github.com/KhronosGroup/glTF-Blender-IO/issues/1454#issuecomment-928319444 + + # If no transmission --> No volume + transmission_enabled = False + transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission') + if isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked: + transmission_enabled = transmission_socket.default_value > 0 + elif gltf2_blender_get.has_image_node_from_socket(transmission_socket): + transmission_enabled = True + + if transmission_enabled is False: + return None, None + + volume_extension = {} + has_thickness_texture = False + thickness_slots = () + + thicknesss_socket = gltf2_blender_get.get_socket_old(blender_material, 'Thickness') + if thicknesss_socket is None: + # If no thickness (here because there is no glTF Material Output node), no volume extension export + return None, None + + density_socket = gltf2_blender_get.get_socket(blender_material, 'Density', volume=True) + attenuation_color_socket = gltf2_blender_get.get_socket(blender_material, 'Color', volume=True) + # Even if density or attenuation are not set, we export volume extension + + if isinstance(attenuation_color_socket, bpy.types.NodeSocket): + rgb = gltf2_blender_get.get_const_from_default_value_socket(attenuation_color_socket, kind='RGB') + volume_extension['attenuationColor'] = rgb + + if isinstance(density_socket, bpy.types.NodeSocket): + density = gltf2_blender_get.get_const_from_default_value_socket(density_socket, kind='VALUE') + volume_extension['attenuationDistance'] = 1.0 / density if density != 0 else None # infinity (Using None as glTF default) + + + if isinstance(thicknesss_socket, bpy.types.NodeSocket) and not thicknesss_socket.is_linked: + val = thicknesss_socket.default_value + if val == 0.0: + # If no thickness, no volume extension export + return None, None + volume_extension['thicknessFactor'] = val + elif gltf2_blender_get.has_image_node_from_socket(thicknesss_socket): + fac = gltf2_blender_get.get_factor_from_socket(thicknesss_socket, kind='VALUE') + # default value in glTF is 0.0, but if there is a texture without factor, use 1 + volume_extension['thicknessFactor'] = fac if fac != None else 1.0 + has_thickness_texture = True + + # Pack thickness channel (R). + if has_thickness_texture: + thickness_slots = (thicknesss_socket,) + + use_actives_uvmaps = [] + + if len(thickness_slots) > 0: + combined_texture, use_active_uvmap, _ = gltf2_blender_gather_texture_info.gather_texture_info( + thicknesss_socket, + thickness_slots, + export_settings, + ) + if has_thickness_texture: + volume_extension['thicknessTexture'] = combined_texture + if use_active_uvmap: + use_actives_uvmaps.append("thicknessTexture") + + return Extension('KHR_materials_volume', volume_extension, False), use_actives_uvmaps
\ No newline at end of file diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index 72f0268c..1af588b9 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -44,7 +44,7 @@ def array_to_accessor(array, component_type, data_type, include_max_and_min=Fals amin = np.amin(array, axis=0).tolist() return gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData(array.tobytes()), + buffer_view=gltf2_io_binary_data.BinaryData(array.tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER), byte_offset=None, component_type=component_type, count=len(array), @@ -124,28 +124,34 @@ def __gather_colors(blender_primitive, export_settings): color_index = 0 color_id = 'COLOR_' + str(color_index) while blender_primitive["attributes"].get(color_id) is not None: - colors = blender_primitive["attributes"][color_id] + colors = blender_primitive["attributes"][color_id]["data"] if type(colors) is not np.ndarray: colors = np.array(colors, dtype=np.float32) colors = colors.reshape(len(colors) // 4, 4) - # Convert to normalized ushorts - colors *= 65535 - colors += 0.5 # bias for rounding - colors = colors.astype(np.uint16) + if blender_primitive["attributes"][color_id]["norm"] is True: + comp_type = gltf2_io_constants.ComponentType.UnsignedShort + + # Convert to normalized ushorts + colors *= 65535 + colors += 0.5 # bias for rounding + colors = colors.astype(np.uint16) + + else: + comp_type = gltf2_io_constants.ComponentType.Float attributes[color_id] = gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData(colors.tobytes()), + buffer_view=gltf2_io_binary_data.BinaryData(colors.tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER), byte_offset=None, - component_type=gltf2_io_constants.ComponentType.UnsignedShort, + component_type=comp_type, count=len(colors), extensions=None, extras=None, max=None, min=None, name=None, - normalized=True, + normalized=blender_primitive["attributes"][color_id]["norm"], sparse=None, type=gltf2_io_constants.DataType.Vec4, ) @@ -167,6 +173,13 @@ def __gather_skins(blender_primitive, export_settings): max_bone_set_index += 1 max_bone_set_index -= 1 + # Here, a set represents a group of 4 weights. + # So max_bone_set_index value: + # if -1 => No weights + # if 1 => Max 4 weights + # if 2 => Max 8 weights + # etc... + # If no skinning if max_bone_set_index < 0: return attributes diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index 367c30f5..576a1418 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -12,10 +12,12 @@ from io_scene_gltf2.blender.exp import gltf2_blender_extract from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_variants from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.io.com import gltf2_io_extensions from io_scene_gltf2.io.com.gltf2_io_debug import print_console @@ -86,7 +88,7 @@ def gather_primitives( primitive = gltf2_io.MeshPrimitive( attributes=internal_primitive['attributes'], - extensions=None, + extensions=__gather_extensions(blender_mesh, material_idx, active_uvmap_idx, export_settings), extras=None, indices=internal_primitive['indices'], material=material, @@ -148,7 +150,7 @@ def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings return None element_type = gltf2_io_constants.DataType.Scalar - binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes()) + binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes(), bufferViewTarget=gltf2_io_constants.BufferViewTarget.ELEMENT_ARRAY_BUFFER) return gltf2_blender_gather_accessors.gather_accessor( binary_data, component_type, @@ -214,3 +216,55 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings morph_index += 1 return targets return None + +def __gather_extensions(blender_mesh, + material_idx: int, + active_uvmap_idx, + export_settings): + extensions = {} + + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False: + return None + + if bpy.data.scenes[0].get('gltf2_KHR_materials_variants_variants') is None: + return None + if len(bpy.data.scenes[0]['gltf2_KHR_materials_variants_variants']) == 0: + return None + + # Material idx is the slot idx. Retrieve associated variant, if any + mapping = [] + for i in [v for v in blender_mesh.gltf2_variant_mesh_data if v.material_slot_index == material_idx]: + variants = [] + for idx, v in enumerate(i.variants): + if v.variant.variant_idx in [o.variant.variant_idx for o in i.variants[:idx]]: + # Avoid duplicates + continue + vari = gltf2_blender_gather_materials_variants.gather_variant(v.variant.variant_idx, export_settings) + if vari is not None: + variant_extension = gltf2_io_extensions.ChildOfRootExtension( + name="KHR_materials_variants", + path=["variants"], + extension=vari + ) + variants.append(variant_extension) + if len(variants) > 0: + if i.material: + mat = gltf2_blender_gather_materials.gather_material( + i.material, + active_uvmap_idx, + export_settings + ) + else: + # empty slot + mat = None + mapping.append({'material': mat, 'variants': variants}) + + if len(mapping) > 0: + extensions["KHR_materials_variants"] = gltf2_io_extensions.Extension( + name="KHR_materials_variants", + extension={ + "mappings": mapping + } + ) + + return extensions if extensions else None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py index e8c6baf1..ccfd42e5 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py @@ -26,24 +26,26 @@ def gather_texture( """ if not __filter_texture(blender_shader_sockets, export_settings): - return None + return None, None + + source, factor = __gather_source(blender_shader_sockets, export_settings) texture = gltf2_io.Texture( extensions=__gather_extensions(blender_shader_sockets, export_settings), extras=__gather_extras(blender_shader_sockets, export_settings), name=__gather_name(blender_shader_sockets, export_settings), sampler=__gather_sampler(blender_shader_sockets, export_settings), - source=__gather_source(blender_shader_sockets, export_settings) + source= source ) # although valid, most viewers can't handle missing source properties # This can have None source for "keep original", when original can't be found if texture.source is None: - return None + return None, None export_user_extensions('gather_texture_hook', export_settings, texture, blender_shader_sockets) - return texture + return texture, factor def __filter_texture(blender_shader_sockets, export_settings): @@ -66,13 +68,14 @@ def __gather_name(blender_shader_sockets, export_settings): def __gather_sampler(blender_shader_sockets, export_settings): - shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets] + shader_nodes = [__get_tex_from_socket(socket) for socket in blender_shader_sockets] if len(shader_nodes) > 1: gltf2_io_debug.print_console("WARNING", "More than one shader node tex image used for a texture. " "The resulting glTF sampler will behave like the first shader node tex image.") + first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes)).shader_node return gltf2_blender_gather_sampler.gather_sampler( - shader_nodes[0], + first_valid_shader_node, export_settings) @@ -81,7 +84,7 @@ def __gather_source(blender_shader_sockets, export_settings): # Helpers - +# TODOExt deduplicate def __get_tex_from_socket(socket): result = gltf2_blender_search_node_tree.from_socket( socket, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py index 15b101ad..5fe2da32 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py @@ -19,14 +19,14 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension # occlusion the primary_socket would be the occlusion socket, and # blender_shader_sockets would be the (O,R,M) sockets. -def gather_texture_info(primary_socket, blender_shader_sockets, export_settings): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'DEFAULT', export_settings) +def gather_texture_info(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'): + return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'DEFAULT', filter_type, export_settings) -def gather_material_normal_texture_info_class(primary_socket, blender_shader_sockets, export_settings): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'NORMAL', export_settings) +def gather_material_normal_texture_info_class(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'): + return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'NORMAL', filter_type, export_settings) -def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_sockets, export_settings): - return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'OCCLUSION', export_settings) +def gather_material_occlusion_texture_info_class(primary_socket, blender_shader_sockets, export_settings, filter_type='ALL'): + return __gather_texture_info_helper(primary_socket, blender_shader_sockets, 'OCCLUSION', filter_type, export_settings) @cached @@ -34,16 +34,19 @@ def __gather_texture_info_helper( primary_socket: bpy.types.NodeSocket, blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket], kind: str, + filter_type: str, export_settings): - if not __filter_texture_info(primary_socket, blender_shader_sockets, export_settings): - return None, None + if not __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings): + return None, None, None tex_transform, tex_coord, use_active_uvmap = __gather_texture_transform_and_tex_coord(primary_socket, export_settings) + index, factor = __gather_index(blender_shader_sockets, export_settings) + fields = { 'extensions': __gather_extensions(tex_transform, export_settings), 'extras': __gather_extras(blender_shader_sockets, export_settings), - 'index': __gather_index(blender_shader_sockets, export_settings), + 'index': index, 'tex_coord': tex_coord } @@ -59,14 +62,14 @@ def __gather_texture_info_helper( texture_info = gltf2_io.MaterialOcclusionTextureInfoClass(**fields) if texture_info.index is None: - return None, None + return None, None, None export_user_extensions('gather_texture_info_hook', export_settings, texture_info, blender_shader_sockets) - return texture_info, use_active_uvmap + return texture_info, use_active_uvmap, factor -def __filter_texture_info(primary_socket, blender_shader_sockets, export_settings): +def __filter_texture_info(primary_socket, blender_shader_sockets, filter_type, export_settings): if primary_socket is None: return False if __get_tex_from_socket(primary_socket) is None: @@ -75,9 +78,18 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, export_setting return False if not all([elem is not None for elem in blender_shader_sockets]): return False - if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets]): - # sockets do not lead to a texture --> discard - return False + if filter_type == "ALL": + # Check that all sockets link to texture + if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets]): + # sockets do not lead to a texture --> discard + return False + elif filter_type == "ANY": + # Check that at least one socket link to texture + if all([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets]): + return False + elif filter_type == "NONE": + # No check + pass return True @@ -163,7 +175,7 @@ def __gather_texture_transform_and_tex_coord(primary_socket, export_settings): return texture_transform, texcoord_idx or None, use_active_uvmap - +# TODOExt deduplicate def __get_tex_from_socket(socket): result = gltf2_blender_search_node_tree.from_socket( socket, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py index ba63e049..c654b445 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py @@ -95,10 +95,18 @@ class VExportTree: bpy.context.window.scene = blender_scene depsgraph = bpy.context.evaluated_depsgraph_get() + # Gather parent/children information once, as calling bobj.children is + # very expensive operation : takes O(len(bpy.data.objects)) time. + blender_children = dict() + for bobj in bpy.data.objects: + bparent = bobj.parent + blender_children.setdefault(bobj, []) + blender_children.setdefault(bparent, []).append(bobj) + for blender_object in [obj.original for obj in depsgraph.scene_eval.objects if obj.parent is None]: - self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4)) + self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4), blender_children) - def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, armature_uuid=None, dupli_world_matrix=None): + def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, blender_children, armature_uuid=None, dupli_world_matrix=None): node = VExportNode() node.uuid = str(uuid.uuid4()) node.parent_uuid = parent_uuid @@ -163,10 +171,16 @@ class VExportTree: # So real world matrix is collection world_matrix @ "world_matrix" of object node.matrix_world = parent_coll_matrix_world @ blender_object.matrix_world.copy() if node.blender_type == VExportNode.CAMERA and self.export_settings[gltf2_blender_export_keys.CAMERAS]: - correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + if self.export_settings[gltf2_blender_export_keys.YUP]: + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + else: + correction = Matrix.Identity(4).to_quaternion() node.matrix_world @= correction.to_matrix().to_4x4() elif node.blender_type == VExportNode.LIGHT and self.export_settings[gltf2_blender_export_keys.LIGHTS]: - correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + if self.export_settings[gltf2_blender_export_keys.YUP]: + correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0)) + else: + correction = Matrix.Identity(4).to_quaternion() node.matrix_world @= correction.to_matrix().to_4x4() elif node.blender_type == VExportNode.BONE: if self.export_settings['gltf_current_frame'] is True: @@ -193,42 +207,42 @@ class VExportTree: # standard children if blender_bone is None and blender_object.is_instancer is False: - for child_object in blender_object.children: + for child_object in blender_children[blender_object]: if child_object.parent_bone: # Object parented to bones # Will be manage later continue else: # Classic parenting - self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world) + self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world, blender_children) # Collections if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: for dupli_object in blender_object.instance_collection.all_objects: if dupli_object.parent is not None: continue - self.recursive_node_traverse(dupli_object, None, node.uuid, node.matrix_world) + self.recursive_node_traverse(dupli_object, None, node.uuid, node.matrix_world, blender_children) # Armature : children are bones with no parent if blender_object.type == "ARMATURE" and blender_bone is None: for b in [b for b in blender_object.pose.bones if b.parent is None]: - self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, node.uuid) + self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, blender_children, node.uuid) # Bones if blender_object.type == "ARMATURE" and blender_bone is not None: for b in blender_bone.children: - self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, armature_uuid) + self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, blender_children, armature_uuid) # Object parented to bone if blender_bone is not None: - for child_object in [c for c in blender_object.children if c.parent_bone is not None and c.parent_bone == blender_bone.name]: - self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world) + for child_object in [c for c in blender_children[blender_object] if c.parent_bone is not None and c.parent_bone == blender_bone.name]: + self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world, blender_children) # Duplis if blender_object.is_instancer is True and blender_object.instance_type != 'COLLECTION': depsgraph = bpy.context.evaluated_depsgraph_get() for (dupl, mat) in [(dup.object.original, dup.matrix_world.copy()) for dup in depsgraph.object_instances if dup.parent and id(dup.parent.original) == id(blender_object)]: - self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, dupli_world_matrix=mat) + self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, blender_children, dupli_world_matrix=mat) def get_all_objects(self): return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE] @@ -462,3 +476,20 @@ class VExportTree: skin = gather_skin(n.uuid, self.export_settings) skins.append(skin) return skins + + def variants_reset_to_original(self): + # Only if Variants are displayed and exported + if bpy.context.preferences.addons['io_scene_gltf2'].preferences.KHR_materials_variants_ui is False: + return + objects = [self.nodes[o].blender_object for o in self.get_all_node_of_type(VExportNode.OBJECT) if self.nodes[o].blender_object.type == "MESH" \ + and self.nodes[o].blender_object.data.get('gltf2_variant_default_materials') is not None] + for obj in objects: + # loop on material slots ( primitives ) + for mat_slot_idx, s in enumerate(obj.material_slots): + # Check if there is a default material for this slot + for i in obj.data.gltf2_variant_default_materials: + if i.material_slot_index == mat_slot_idx: + s.material = i.default_material + break + + # If not found, keep current material as default diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py index e38906e6..9e468186 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_get.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py @@ -4,9 +4,10 @@ import bpy from mathutils import Vector, Matrix -from ..com.gltf2_blender_material_helpers import get_gltf_node_name +from ..com.gltf2_blender_material_helpers import get_gltf_node_name, get_gltf_node_old_name from ...blender.com.gltf2_blender_conversion import texture_transform_blender_to_gltf from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree def get_animation_target(action_group: bpy.types.ActionGroup): @@ -47,7 +48,7 @@ def get_node_socket(blender_material, type, name): return None -def get_socket(blender_material: bpy.types.Material, name: str): +def get_socket(blender_material: bpy.types.Material, name: str, volume=False): """ For a given material input name, retrieve the corresponding node tree socket. @@ -70,8 +71,15 @@ def get_socket(blender_material: bpy.types.Material, name: str): elif name == "Background": type = bpy.types.ShaderNodeBackground name = "Color" + elif name == "sheenColor": + return get_node_socket(blender_material, bpy.types.ShaderNodeBsdfVelvet, "Color") + elif name == "sheenRoughness": + return get_node_socket(blender_material, bpy.types.ShaderNodeBsdfVelvet, "Sigma") else: - type = bpy.types.ShaderNodeBsdfPrincipled + if volume is False: + type = bpy.types.ShaderNodeBsdfPrincipled + else: + type = bpy.types.ShaderNodeVolumeAbsorption return get_node_socket(blender_material, type, name) @@ -86,11 +94,11 @@ def get_socket_old(blender_material: bpy.types.Material, name: str): :param name: the name of the socket :return: a blender NodeSocket """ - gltf_node_group_name = get_gltf_node_name().lower() + gltf_node_group_names = [get_gltf_node_name().lower(), get_gltf_node_old_name().lower()] if blender_material.node_tree and blender_material.use_nodes: nodes = [n for n in blender_material.node_tree.nodes if \ isinstance(n, bpy.types.ShaderNodeGroup) and \ - (n.node_tree.name.startswith('glTF Metallic Roughness') or n.node_tree.name.lower() == gltf_node_group_name)] + (n.node_tree.name.startswith('glTF Metallic Roughness') or n.node_tree.name.lower() in gltf_node_group_names)] inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) if inputs: return inputs[0] @@ -297,3 +305,12 @@ def previous_node(socket): if prev_socket is not None: return prev_socket.node return None + +#TODOExt is this the same as __get_tex_from_socket from gather_image ? +def has_image_node_from_socket(socket): + result = gltf2_blender_search_node_tree.from_socket( + socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return False + return True diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_image.py index 8b9db89a..6730f479 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_image.py @@ -27,6 +27,18 @@ class FillWhite: """Fills a channel with all ones (1.0).""" pass +class StoreData: + def __init__(self, data): + """Store numeric data (not an image channel""" + self.data = data + +class StoreImage: + """ + Store a channel with the channel src_chan from a Blender image. + This channel will be used for numpy calculation (no direct channel mapping) + """ + def __init__(self, image: bpy.types.Image): + self.image = image class ExportImage: """Custom image class. @@ -55,9 +67,13 @@ class ExportImage: def __init__(self, original=None): self.fills = {} + self.stored = {} - # In case of keeping original texture images - self.original = original + self.original = original # In case of keeping original texture images + self.numpy_calc = None + + def set_calc(self, numpy_calc): + self.numpy_calc = numpy_calc # In case of numpy calculation (no direct channel mapping) @staticmethod def from_blender_image(image: bpy.types.Image): @@ -73,6 +89,12 @@ class ExportImage: def fill_image(self, image: bpy.types.Image, dst_chan: Channel, src_chan: Channel): self.fills[dst_chan] = FillImage(image, src_chan) + def store_data(self, identifier, data, type='Image'): + if type == "Image": # This is an image + self.stored[identifier] = StoreImage(data) + else: # This is a numeric value + self.stored[identifier] = StoreData(data) + def fill_white(self, dst_chan: Channel): self.fills[dst_chan] = FillWhite() @@ -81,7 +103,7 @@ class ExportImage: def empty(self) -> bool: if self.original is None: - return not self.fills + return not (self.fills or self.stored) else: return False @@ -103,7 +125,7 @@ class ExportImage: len(set(fill.image.name for fill in self.fills.values())) == 1 ) - def encode(self, mime_type: Optional[str]) -> bytes: + def encode(self, mime_type: Optional[str]) -> Tuple[bytes, bool]: self.file_format = { "image/jpeg": "JPEG", "image/png": "PNG" @@ -111,10 +133,14 @@ class ExportImage: # Happy path = we can just use an existing Blender image if self.__on_happy_path(): - return self.__encode_happy() + return self.__encode_happy(), None - # Unhappy path = we need to create the image self.fills describes. - return self.__encode_unhappy() + # Unhappy path = we need to create the image self.fills describes or self.stores describes + if self.numpy_calc is None: + return self.__encode_unhappy(), None + else: + pixels, width, height, factor = self.numpy_calc(self.stored) + return self.__encode_from_numpy_array(pixels, (width, height)), factor def __encode_happy(self) -> bytes: return self.__encode_from_image(self.blender_image()) @@ -147,7 +173,7 @@ class ExportImage: else: # Image is the wrong size; make a temp copy and scale it. with TmpImageGuard() as guard: - _make_temp_image_copy(guard, src_image=image) + make_temp_image_copy(guard, src_image=image) tmp_image = guard.image tmp_image.scale(width, height) tmp_image.pixels.foreach_get(tmp_buf) @@ -197,7 +223,7 @@ class ExportImage: # Copy to a temp image and save. with TmpImageGuard() as guard: - _make_temp_image_copy(guard, src_image=image) + make_temp_image_copy(guard, src_image=image) tmp_image = guard.image return _encode_temp_image(tmp_image, self.file_format) @@ -228,7 +254,7 @@ class TmpImageGuard: bpy.data.images.remove(self.image, do_unlink=True) -def _make_temp_image_copy(guard: TmpImageGuard, src_image: bpy.types.Image): +def make_temp_image_copy(guard: TmpImageGuard, src_image: bpy.types.Image): """Makes a temporary copy of src_image. Will be cleaned up with guard.""" guard.image = src_image.copy() tmp_image = guard.image diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py b/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py new file mode 100644 index 00000000..6321f128 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_texture_specular.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2018-2022 The glTF-Blender-IO authors. + +import bpy +import numpy as np +from .gltf2_blender_gather_image import StoreImage, StoreData +from .gltf2_blender_image import TmpImageGuard, make_temp_image_copy + +def specular_calculation(stored): + + # See https://gist.github.com/proog128/d627c692a6bbe584d66789a5a6437a33 + + # Find all Blender images used + images = [] + for fill in stored.values(): + if isinstance(fill, StoreImage): + if fill.image not in images: + images.append(fill.image) + + if not images: + # No ImageFills; use a 1x1 white pixel + pixels = np.array([1.0, 1.0, 1.0, 1.0], np.float32) + return pixels, 1, 1 + + width = max(image.size[0] for image in images) + height = max(image.size[1] for image in images) + + buffers = {} + + for identifier, image in [(ident, store.image) for (ident, store) in stored.items() if isinstance(store, StoreImage)]: + tmp_buf = np.empty(width * height * 4, np.float32) + + if image.size[0] == width and image.size[1] == height: + image.pixels.foreach_get(tmp_buf) + else: + # Image is the wrong size; make a temp copy and scale it. + with TmpImageGuard() as guard: + make_temp_image_copy(guard, src_image=image) + tmp_image = guard.image + tmp_image.scale(width, height) + tmp_image.pixels.foreach_get(tmp_buf) + + buffers[identifier] = np.reshape(tmp_buf, [width, height, 4]) + + # keep only needed channels + ## scalar + for i in ['specular', 'specular_tint', 'transmission']: + if i in buffers.keys(): + buffers[i] = buffers[i][:,:,stored[i + "_channel"].data] + else: + buffers[i] = np.full((width, height, 1), stored[i].data) + + # Vector 3 + for i in ['base_color']: + if i in buffers.keys(): + if i + "_channel" not in stored.keys(): + buffers[i] = buffers[i][:,:,:3] + else: + # keep only needed channel + for c in range(3): + if c != stored[i+"_channel"].data: + buffers[i][:, :, c] = 0.0 + buffers[i] = buffers[i][:,:,:3] + else: + buffers[i] = np.full((width, height, 3), stored[i].data[0:3]) + + ior = stored['ior'].data + + # calculation + stack3 = lambda v: np.dstack([v]*3) + + def normalize(c): + luminance = lambda c: 0.3 * c[:,:,0] + 0.6 * c[:,:,1] + 0.1 * c[:,:,2] + l = luminance(c) + # TODOExt Manage all 0 + return c / stack3(l) + + + f0_from_ior = ((ior - 1)/(ior + 1))**2 + tint_strength = (1 - stack3(buffers['specular_tint'])) + normalize(buffers['base_color']) * stack3(buffers['specular_tint']) + out_buf = (1 - stack3(buffers['transmission'])) * (1 / f0_from_ior) * 0.08 * stack3(buffers['specular']) * tint_strength + stack3(buffers['transmission']) * tint_strength + + # Manage values > 1.0 -> Need to apply factor + factor = None + factors = [np.amax(out_buf[:, :, i]) for i in range(3)] + + if any([f > 1.0 for f in factors]): + factor = [1.0 if f < 1.0 else f for f in factors] + out_buf /= factor + + out_buf = np.dstack((out_buf, np.ones((width, height)))) # Set alpha (glTF specular) to 1 + out_buf = np.reshape(out_buf, (width * height * 4)) + + return np.float32(out_buf), width, height, [float(f) for f in factor] if factor else None |