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:
Julien Duroure 2019-01-09 19:46:45 +01:00
parent 52111c31d7
commit b410a70fa9
11 changed files with 115 additions and 33 deletions

View File

@ -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
#

View File

@ -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

View File

@ -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.

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)]

View File

@ -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=[]):

View File

@ -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