glTF export: enhancement & fixes:
* implement KHR_materials_unlit export * Fix primitive restart values * Fix jpg uri/mime type * Fix bug when image has no color channels * Check animation has actions * Ignore meshes without primitives * Fix materials when not selected in export settings * Improve error message for invalid animation target type * Animation with errors are ignored, but export continues * Export of BaseColorFactor
This commit is contained in:
parent
52111c31d7
commit
b410a70fa9
|
@ -975,7 +975,12 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp
|
|||
|
||||
#
|
||||
|
||||
range_indices = 65536
|
||||
# NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed.
|
||||
# Specifically, the value 65535 (in UINT16) cannot be used as a vertex index.
|
||||
# https://github.com/KhronosGroup/glTF/issues/1142
|
||||
# https://github.com/KhronosGroup/glTF/pull/1476/files
|
||||
|
||||
range_indices = 65535
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class Keyframe:
|
|||
}.get(self.__target)
|
||||
|
||||
if length is None:
|
||||
raise RuntimeError("Unknown target type {}".format(self.__target))
|
||||
raise RuntimeError("Animations with target type '{}' are not supported.".format(self.__target))
|
||||
|
||||
return length
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import typing
|
|||
|
||||
from io_scene_gltf2.io.com import gltf2_io
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels
|
||||
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
|
||||
|
||||
|
||||
def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]:
|
||||
|
@ -48,13 +49,18 @@ def __gather_animation(blender_action: bpy.types.Action,
|
|||
if not __filter_animation(blender_action, blender_object, export_settings):
|
||||
return None
|
||||
|
||||
animation = gltf2_io.Animation(
|
||||
channels=__gather_channels(blender_action, blender_object, export_settings),
|
||||
extensions=__gather_extensions(blender_action, blender_object, export_settings),
|
||||
extras=__gather_extras(blender_action, blender_object, export_settings),
|
||||
name=__gather_name(blender_action, blender_object, export_settings),
|
||||
samplers=__gather_samplers(blender_action, blender_object, export_settings)
|
||||
)
|
||||
name = __gather_name(blender_action, blender_object, export_settings)
|
||||
try:
|
||||
animation = gltf2_io.Animation(
|
||||
channels=__gather_channels(blender_action, blender_object, export_settings),
|
||||
extensions=__gather_extensions(blender_action, blender_object, export_settings),
|
||||
extras=__gather_extras(blender_action, blender_object, export_settings),
|
||||
name=name,
|
||||
samplers=__gather_samplers(blender_action, blender_object, export_settings)
|
||||
)
|
||||
except RuntimeError as error:
|
||||
print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error))
|
||||
return None
|
||||
|
||||
# To allow reuse of samplers in one animation,
|
||||
__link_samplers(animation, export_settings)
|
||||
|
@ -159,7 +165,8 @@ def __get_blender_actions(blender_object: bpy.types.Object
|
|||
if blender_object.type == "MESH" \
|
||||
and blender_object.data is not None \
|
||||
and blender_object.data.shape_keys is not None \
|
||||
and blender_object.data.shape_keys.animation_data is not None:
|
||||
and blender_object.data.shape_keys.animation_data is not None \
|
||||
and blender_object.data.shape_keys.animation_data.action is not None:
|
||||
blender_actions.append(blender_object.data.shape_keys.animation_data.action)
|
||||
|
||||
# Remove duplicate actions.
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import re
|
||||
|
||||
import bpy
|
||||
import typing
|
||||
|
@ -30,13 +31,17 @@ def gather_image(
|
|||
export_settings):
|
||||
if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings):
|
||||
return None
|
||||
|
||||
uri = __gather_uri(blender_shader_sockets_or_texture_slots, export_settings)
|
||||
mime_type = __gather_mime_type(uri.filepath if uri is not None else "")
|
||||
|
||||
image = gltf2_io.Image(
|
||||
buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings),
|
||||
extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
|
||||
extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
|
||||
mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings),
|
||||
mime_type=mime_type,
|
||||
name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
|
||||
uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings)
|
||||
uri=uri
|
||||
)
|
||||
return image
|
||||
|
||||
|
@ -51,7 +56,7 @@ def __gather_buffer_view(sockets_or_slots, export_settings):
|
|||
if export_settings[gltf2_blender_export_keys.FORMAT] != 'GLTF_SEPARATE':
|
||||
image = __get_image_data(sockets_or_slots, export_settings)
|
||||
return gltf2_io_binary_data.BinaryData(
|
||||
data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings)))
|
||||
data=image.to_image_data(__gather_mime_type()))
|
||||
return None
|
||||
|
||||
|
||||
|
@ -63,9 +68,13 @@ def __gather_extras(sockets_or_slots, export_settings):
|
|||
return None
|
||||
|
||||
|
||||
def __gather_mime_type(sockets_or_slots, export_settings):
|
||||
return 'image/png'
|
||||
# return 'image/jpeg'
|
||||
def __gather_mime_type(filepath=""):
|
||||
extension_types = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg'}
|
||||
default_extension = extension_types['.png']
|
||||
|
||||
matches = re.findall(r'\.\w+$', filepath)
|
||||
extension = matches[0] if len(matches) > 0 else default_extension
|
||||
return extension_types[extension] if extension.lower() in extension_types.keys() else default_extension
|
||||
|
||||
|
||||
def __gather_name(sockets_or_slots, export_settings):
|
||||
|
@ -98,6 +107,8 @@ def __get_image_data(sockets_or_slots, export_settings):
|
|||
# in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
|
||||
# ressources.
|
||||
def split_pixels_by_channels(image: bpy.types.Image, export_settings) -> typing.List[typing.List[float]]:
|
||||
assert image.channels > 0, "Image '{}' has no color channels and cannot be exported.".format(image.name)
|
||||
|
||||
channelcache = export_settings['gltf_channelcache']
|
||||
if image.name in channelcache:
|
||||
return channelcache[image.name]
|
||||
|
|
|
@ -16,7 +16,8 @@ import bpy
|
|||
|
||||
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
|
||||
from io_scene_gltf2.io.com import gltf2_io
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
|
||||
from io_scene_gltf2.io.com.gltf2_io_extensions import Extension
|
||||
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_gather_material_normal_texture_info_class
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class
|
||||
|
||||
|
@ -69,11 +70,7 @@ def gather_material(blender_material, export_settings):
|
|||
|
||||
|
||||
def __filter_material(blender_material, export_settings):
|
||||
# if not blender_material.use_nodes:
|
||||
# return False
|
||||
# if not blender_material.node_tree:
|
||||
# return False
|
||||
return True
|
||||
return export_settings[gltf2_blender_export_keys.MATERIALS]
|
||||
|
||||
|
||||
def __gather_alpha_cutoff(blender_material, export_settings):
|
||||
|
@ -113,6 +110,8 @@ def __gather_emissive_texture(blender_material, export_settings):
|
|||
def __gather_extensions(blender_material, export_settings):
|
||||
extensions = {}
|
||||
|
||||
if gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") is not None:
|
||||
extensions["KHR_materials_unlit"] = Extension("KHR_materials_unlit", {}, False)
|
||||
|
||||
# TODO specular glossiness extension
|
||||
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
# limitations under the License.
|
||||
|
||||
import bpy
|
||||
from mathutils import Color
|
||||
|
||||
from io_scene_gltf2.io.com import gltf2_io
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_search_node_tree
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_get
|
||||
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
|
||||
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
|
||||
|
||||
|
||||
@cached
|
||||
|
@ -48,9 +50,41 @@ def __gather_base_color_factor(blender_material, export_settings):
|
|||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
|
||||
if base_color_socket is None:
|
||||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColorFactor")
|
||||
if isinstance(base_color_socket, bpy.types.NodeSocket) and not base_color_socket.is_linked:
|
||||
if base_color_socket is None:
|
||||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background")
|
||||
if not isinstance(base_color_socket, bpy.types.NodeSocket):
|
||||
return None
|
||||
if not base_color_socket.is_linked:
|
||||
return list(base_color_socket.default_value)
|
||||
return None
|
||||
|
||||
texture_node = __get_tex_from_socket(base_color_socket)
|
||||
if texture_node is None:
|
||||
return None
|
||||
|
||||
def is_valid_multiply_node(node):
|
||||
return isinstance(node, bpy.types.ShaderNodeMixRGB) and \
|
||||
node.blend_type == "MULTIPLY" and \
|
||||
len(node.inputs) == 3
|
||||
|
||||
multiply_node = next((link.from_node for link in texture_node.path if is_valid_multiply_node(link.from_node)), None)
|
||||
if multiply_node is None:
|
||||
return None
|
||||
|
||||
def is_factor_socket(socket):
|
||||
return isinstance(socket, bpy.types.NodeSocketColor) and \
|
||||
(not socket.is_linked or socket.links[0] not in texture_node.path)
|
||||
|
||||
factor_socket = next((socket for socket in multiply_node.inputs if is_factor_socket(socket)), None)
|
||||
if factor_socket is None:
|
||||
return None
|
||||
|
||||
if factor_socket.is_linked:
|
||||
print_console("WARNING", "BaseColorFactor only supports sockets without links (in Node '{}')."
|
||||
.format(multiply_node.name))
|
||||
return None
|
||||
|
||||
return list(factor_socket.default_value)
|
||||
|
||||
|
||||
def __gather_base_color_texture(blender_material, export_settings):
|
||||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
|
||||
|
@ -58,9 +92,20 @@ def __gather_base_color_texture(blender_material, export_settings):
|
|||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
|
||||
if base_color_socket is None:
|
||||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColor")
|
||||
if base_color_socket is None:
|
||||
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background")
|
||||
return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings)
|
||||
|
||||
|
||||
def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket):
|
||||
result = gltf2_blender_search_node_tree.from_socket(
|
||||
blender_shader_socket,
|
||||
gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
|
||||
if not result:
|
||||
return None
|
||||
return result[0]
|
||||
|
||||
|
||||
def __gather_extensions(blender_material, export_settings):
|
||||
return None
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
|
|||
from io_scene_gltf2.io.com import gltf2_io
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives
|
||||
from io_scene_gltf2.blender.exp import gltf2_blender_generate_extras
|
||||
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
|
||||
|
||||
|
||||
@cached
|
||||
|
@ -38,6 +39,9 @@ def gather_mesh(blender_mesh: bpy.types.Mesh,
|
|||
weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings)
|
||||
)
|
||||
|
||||
if len(mesh.primitives) == 0:
|
||||
print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name))
|
||||
return None
|
||||
return mesh
|
||||
|
||||
|
||||
|
|
|
@ -72,13 +72,19 @@ def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settin
|
|||
def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings):
|
||||
indices = blender_primitive['indices']
|
||||
|
||||
# NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed.
|
||||
# Specifically, the values 65535 (in UINT16) and 4294967295 (in UINT32) cannot be used as indices.
|
||||
# https://github.com/KhronosGroup/glTF/issues/1142
|
||||
# https://github.com/KhronosGroup/glTF/pull/1476/files
|
||||
# Also, UINT8 mode is not supported:
|
||||
# https://github.com/KhronosGroup/glTF/issues/1471
|
||||
max_index = max(indices)
|
||||
if max_index < (1 << 16):
|
||||
if max_index < 65535:
|
||||
component_type = gltf2_io_constants.ComponentType.UnsignedShort
|
||||
elif max_index < (1 << 32):
|
||||
elif max_index < 4294967295:
|
||||
component_type = gltf2_io_constants.ComponentType.UnsignedInt
|
||||
else:
|
||||
print_console('ERROR', 'Invalid max_index: ' + str(max_index))
|
||||
print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.')
|
||||
return None
|
||||
|
||||
element_type = gltf2_io_constants.DataType.Scalar
|
||||
|
|
|
@ -48,6 +48,9 @@ def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str):
|
|||
if name == "Emissive":
|
||||
type = bpy.types.ShaderNodeEmission
|
||||
name = "Color"
|
||||
elif name == "Background":
|
||||
type = bpy.types.ShaderNodeBackground
|
||||
name = "Color"
|
||||
else:
|
||||
type = bpy.types.ShaderNodeBsdfPrincipled
|
||||
nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type)]
|
||||
|
|
|
@ -215,7 +215,7 @@ class GlTF2Exporter:
|
|||
# TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the
|
||||
# exporter
|
||||
# TODO: allow embedding of images (base64)
|
||||
return image.name + ".png"
|
||||
return image.name + image.get_extension()
|
||||
|
||||
@classmethod
|
||||
def __get_key_path(cls, d: dict, keypath: List[str], default=[]):
|
||||
|
|
|
@ -82,14 +82,16 @@ def from_socket(start_socket: bpy.types.NodeSocket,
|
|||
for link in start_socket.links:
|
||||
# follow the link to a shader node
|
||||
linked_node = link.from_node
|
||||
# add the link to the current path
|
||||
search_path.append(link)
|
||||
# check if the node matches the filter
|
||||
if shader_node_filter(linked_node):
|
||||
results.append(NodeTreeSearchResult(linked_node, search_path))
|
||||
results.append(NodeTreeSearchResult(linked_node, search_path + [link]))
|
||||
# traverse into inputs of the node
|
||||
for input_socket in linked_node.inputs:
|
||||
results += __search_from_socket(input_socket, shader_node_filter, search_path)
|
||||
linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link])
|
||||
if linked_results:
|
||||
# add the link to the current path
|
||||
search_path.append(link)
|
||||
results += linked_results
|
||||
|
||||
return results
|
||||
|
||||
|
|
Loading…
Reference in New Issue