glTF exporter: various fixes & enhancement

* Fix some Yup conversions
* reading material from glTF node group material if exists
* Fix normal export
* Round transforms near 0 and 1
* Fix exporting from Edit mode
* Various image format management
This commit is contained in:
Julien Duroure 2018-12-18 21:31:29 +01:00
parent 9aa6c8058b
commit bf867f5022
14 changed files with 133 additions and 45 deletions

View File

@ -36,7 +36,7 @@ from bpy.props import (CollectionProperty,
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann & Moritz Becher',
'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann Moritz Becher, Benjamin Schmithüsen',
"version": (0, 0, 1),
'blender': (2, 80, 0),
'location': 'File > Import-Export',

View File

@ -29,7 +29,11 @@ def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Unio
"""Transform a list to blender py object."""
target = get_target_property_name(data_path)
if target == 'location':
if target == 'delta_location':
return Vector(values) # TODO Should be Vector(values) - Vector(something)?
elif target == 'delta_rotation_euler':
return Euler(values).to_quaternion() # TODO Should be multiply(Euler(values).to_quaternion(), something)?
elif target == 'location':
return Vector(values)
elif target == 'rotation_axis_angle':
angle = values[0]
@ -75,6 +79,8 @@ def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.U
"""Manage Yup."""
target = get_target_property_name(data_path)
swizzle_func = {
"delta_location": swizzle_yup_location,
"delta_rotation_euler": swizzle_yup_rotation,
"location": swizzle_yup_location,
"rotation_axis_angle": swizzle_yup_rotation,
"rotation_euler": swizzle_yup_rotation,
@ -114,6 +120,8 @@ def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Ma
"""Manage transformations."""
target = get_target_property_name(data_path)
transform_func = {
"delta_location": transform_location,
"delta_rotation_euler": transform_rotation,
"location": transform_location,
"rotation_axis_angle": transform_rotation,
"rotation_euler": transform_rotation,
@ -157,3 +165,8 @@ def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector:
"""Transform value."""
return value
def round_if_near(value: float, target: float) -> float:
"""If value is very close to target, round to target."""
return value if abs(value - target) > 2.0e-6 else target

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import bpy
import sys
import traceback
@ -25,6 +26,9 @@ from io_scene_gltf2.io.exp import gltf2_io_export
def save(context, export_settings):
"""Start the glTF 2.0 export and saves to content either to a .gltf or .glb file."""
if bpy.context.active_object is not None:
bpy.ops.object.mode_set(mode='OBJECT')
__notify_start(context)
json, buffer = __export(export_settings)
__write_file(json, buffer, export_settings)

View File

@ -648,7 +648,7 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp
bone_count = 0
if vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]:
if blender_vertex_groups is not None and vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]:
joint = []
weight = []
for group_element in vertex.groups:
@ -668,13 +668,15 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp
#
joint_index = 0
modifiers_dict = {m.type: m for m in modifiers}
if "ARMATURE" in modifiers_dict:
armature = modifiers_dict["ARMATURE"].object
skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
for index, j in enumerate(skin.joints):
if j.name == vertex_group_name:
joint_index = index
if modifiers is not None:
modifiers_dict = {m.type: m for m in modifiers}
if "ARMATURE" in modifiers_dict:
armature = modifiers_dict["ARMATURE"].object
skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
for index, j in enumerate(skin.joints):
if j.name == vertex_group_name:
joint_index = index
joint_weight = group_element.weight

View File

@ -67,6 +67,8 @@ def __gather_path(channels: typing.Tuple[bpy.types.FCurve],
) -> str:
target = channels[0].data_path.split('.')[-1]
path = {
"delta_location": "translation",
"delta_rotation_euler": "rotation",
"location": "translation",
"rotation_axis_angle": "rotation",
"rotation_euler": "rotation",

View File

@ -35,6 +35,8 @@ class Keyframe:
def __get_target_len(self):
length = {
"delta_location": 3,
"delta_rotation_euler": 3,
"location": 3,
"rotation_axis_angle": 4,
"rotation_euler": 3,

View File

@ -107,37 +107,40 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
target_datapath = channels[0].data_path
transform = Matrix.Identity(4)
transform = blender_object.matrix_parent_inverse
isYup = export_settings[gltf2_blender_export_keys.YUP]
if blender_object.type == "ARMATURE":
bone = blender_object.path_resolve(get_target_object_path(target_datapath))
if isinstance(bone, bpy.types.PoseBone):
transform = bone.bone.matrix_local
if bone.parent is not None:
parent_transform = bone.parent.bone.matrix_local
transform = gltf2_blender_math.multiply(parent_transform.inverted(), transform)
# if not export_settings[gltf2_blender_export_keys.YUP]:
# transform = gltf2_blender_math.multiply(gltf2_blender_math.to_zup(), transform)
transform = gltf2_blender_math.multiply(transform, parent_transform.inverted())
# if not isYup:
# transform = gltf2_blender_math.multiply(transform, gltf2_blender_math.to_zup())
else:
# only apply the y-up conversion to root bones, as child bones already are in the y-up space
if export_settings[gltf2_blender_export_keys.YUP]:
transform = gltf2_blender_math.multiply(gltf2_blender_math.to_yup(), transform)
if isYup:
transform = gltf2_blender_math.multiply(transform, gltf2_blender_math.to_yup())
local_transform = bone.bone.matrix_local
transform = gltf2_blender_math.multiply(transform, local_transform)
values = []
for keyframe in keyframes:
# Transform the data and extract
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform)
if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
if isYup and not blender_object.type == "ARMATURE":
value = gltf2_blender_math.swizzle_yup(value, target_datapath)
keyframe_value = gltf2_blender_math.mathutils_to_gltf(value)
if keyframe.in_tangent is not None:
in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform)
if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
if isYup and not blender_object.type == "ARMATURE":
in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath)
keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value
if keyframe.out_tangent is not None:
out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform)
if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
if isYup and not blender_object.type == "ARMATURE":
out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath)
keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent)
values += keyframe_value

View File

@ -156,7 +156,7 @@ def __get_blender_actions(blender_object: bpy.types.Object
for strip in track.strips:
blender_actions.append(strip.action)
if blender_object.type == "MESH"\
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:

View File

@ -41,7 +41,7 @@ def gather_material(blender_material, export_settings):
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_emmissive_factor(blender_material, export_settings),
emissive_factor=__gather_emissive_factor(blender_material, export_settings),
emissive_texture=__gather_emissive_texture(blender_material, export_settings),
extensions=__gather_extensions(blender_material, export_settings),
extras=__gather_extras(blender_material, export_settings),
@ -94,8 +94,10 @@ def __gather_double_sided(blender_material, export_settings):
return None
def __gather_emmissive_factor(blender_material, export_settings):
def __gather_emissive_factor(blender_material, export_settings):
emissive_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
if emissive_socket is None:
emissive_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "EmissiveFactor")
if isinstance(emissive_socket, bpy.types.NodeSocket) and not emissive_socket.is_linked:
return list(emissive_socket.default_value)[0:3]
return None
@ -103,6 +105,8 @@ def __gather_emmissive_factor(blender_material, export_settings):
def __gather_emissive_texture(blender_material, export_settings):
emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
if emissive is None:
emissive = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "Emissive")
return gltf2_blender_gather_texture_info.gather_texture_info((emissive,), export_settings)
@ -127,15 +131,19 @@ def __gather_name(blender_material, export_settings):
def __gather_normal_texture(blender_material, export_settings):
normal = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Normal")
if normal is None:
normal = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "Normal")
return gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class(
(normal,),
export_settings)
def __gather_occlusion_texture(blender_material, export_settings):
emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion")
occlusion = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion")
if occlusion is None:
occlusion = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "Occlusion")
return gltf2_blender_gather_material_occlusion_texture_info_class.gather_material_occlusion_texture_info_class(
(emissive,),
(occlusion,),
export_settings)

View File

@ -46,6 +46,8 @@ def __gather_base_color_factor(blender_material, export_settings):
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
if base_color_socket is None:
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:
return list(base_color_socket.default_value)
return None
@ -54,6 +56,8 @@ def __gather_base_color_texture(blender_material, export_settings):
base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
if base_color_socket is None:
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")
return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings)
@ -67,6 +71,8 @@ def __gather_extras(blender_material, export_settings):
def __gather_metallic_factor(blender_material, export_settings):
metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
if metallic_socket is None:
metallic_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "MetallicFactor")
if isinstance(metallic_socket, bpy.types.NodeSocket) and not metallic_socket.is_linked:
return metallic_socket.default_value
return None
@ -78,6 +84,8 @@ def __gather_metallic_roughness_texture(blender_material, export_settings):
if metallic_socket is None and roughness_socket is None:
metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "MetallicRoughness")
if metallic_roughness is None:
metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "MetallicRoughness")
texture_input = (metallic_roughness,)
else:
texture_input = (metallic_socket, roughness_socket)
@ -87,6 +95,8 @@ def __gather_metallic_roughness_texture(blender_material, export_settings):
def __gather_roughness_factor(blender_material, export_settings):
roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
if roughness_socket is None:
roughness_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "RoughnessFactor")
if isinstance(roughness_socket, bpy.types.NodeSocket) and not roughness_socket.is_linked:
return roughness_socket.default_value
return None

View File

@ -17,6 +17,7 @@ import bpy
from mathutils import Quaternion
from . import gltf2_blender_export_keys
from io_scene_gltf2.blender.com import gltf2_blender_math
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras
@ -50,16 +51,17 @@ def gather_node(blender_object, export_settings):
)
node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings)
if blender_object.type == 'LIGHT' and export_settings[gltf2_blender_export_keys.LIGHTS]:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.extensions = {"KHR_lights_punctual": node.extensions["KHR_lights_punctual"]}
del node.extensions["KHR_lights_punctual"]
node.children.append(correction_node)
if blender_object.type == 'CAMERA' and export_settings[gltf2_blender_export_keys.CAMERAS]:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.camera = node.camera
node.children.append(correction_node)
node.camera = None
if export_settings[gltf2_blender_export_keys.YUP]:
if blender_object.type == 'LIGHT' and export_settings[gltf2_blender_export_keys.LIGHTS]:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.extensions = {"KHR_lights_punctual": node.extensions["KHR_lights_punctual"]}
del node.extensions["KHR_lights_punctual"]
node.children.append(correction_node)
if blender_object.type == 'CAMERA' and export_settings[gltf2_blender_export_keys.CAMERAS]:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.camera = node.camera
node.children.append(correction_node)
node.camera = None
return node
@ -153,8 +155,18 @@ def __gather_mesh(blender_object, export_settings):
modifiers = None
if export_settings[gltf2_blender_export_keys.APPLY]:
auto_smooth = blender_object.data.use_auto_smooth
if auto_smooth:
blender_object = blender_object.copy()
edge_split = blender_object.modifiers.new('Temporary_Auto_Smooth', 'EDGE_SPLIT')
edge_split.split_angle = blender_object.data.auto_smooth_angle
edge_split.use_edge_angle = not blender_object.data.has_custom_normals
blender_mesh = blender_object.to_mesh(bpy.context.depsgraph, True)
skip_filter = True
if auto_smooth:
bpy.data.objects.remove(blender_object)
else:
blender_mesh = blender_object.data
skip_filter = False
@ -179,6 +191,12 @@ def __gather_trans_rot_scale(blender_object, export_settings):
trans = -gltf2_blender_extract.convert_swizzle_location(
blender_object.instance_collection.instance_offset, export_settings)
translation, rotation, scale = (None, None, None)
trans[0], trans[1], trans[2] = gltf2_blender_math.round_if_near(trans[0], 0.0), gltf2_blender_math.round_if_near(trans[1], 0.0), \
gltf2_blender_math.round_if_near(trans[2], 0.0)
rot[0], rot[1], rot[2], rot[3] = gltf2_blender_math.round_if_near(rot[0], 0.0), gltf2_blender_math.round_if_near(rot[1], 0.0), \
gltf2_blender_math.round_if_near(rot[2], 0.0), gltf2_blender_math.round_if_near(rot[3], 1.0)
sca[0], sca[1], sca[2] = gltf2_blender_math.round_if_near(sca[0], 1.0), gltf2_blender_math.round_if_near(sca[1], 1.0), \
gltf2_blender_math.round_if_near(sca[2], 1.0)
if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
translation = [trans[0], trans[1], trans[2]]
if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:

View File

@ -43,24 +43,42 @@ def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str):
:return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise
"""
if blender_material.node_tree and blender_material.use_nodes:
i = [input for input in blender_material.node_tree.inputs]
o = [output for output in blender_material.node_tree.outputs]
nodes = [node for node in blender_material.node_tree.nodes]
#i = [input for input in blender_material.node_tree.inputs]
#o = [output for output in blender_material.node_tree.outputs]
if name == "Emissive":
nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeEmission), nodes)
type = bpy.types.ShaderNodeEmission
name = "Color"
else:
nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeBsdfPrincipled), nodes)
type = bpy.types.ShaderNodeBsdfPrincipled
nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type)]
inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
if not inputs:
return None
return inputs[0]
if inputs:
return inputs[0]
return None
def get_socket_or_texture_slot_old(blender_material: bpy.types.Material, name: str):
"""
For a given material input name, retrieve the corresponding node tree socket in the special glTF Metallic Roughness nodes (which might be deprecated?).
:param blender_material: a blender material for which to get the socket/slot
:param name: the name of the socket/slot
:return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise
"""
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')]
inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
if inputs:
return inputs[0]
return None
def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
"""Find any ShaderNodeTexImage in the path from the socket."""
if shader_socket is None:

View File

@ -144,8 +144,7 @@ class GlTF2Exporter:
:return:
"""
for image in self.__images:
dst_path = output_path + image.name + ".png"
dst_path = output_path + image.name + image.get_extension()
src_path = bpy.path.abspath(image.filepath)
if os.path.isfile(src_path):
# Source file exists.

View File

@ -14,6 +14,7 @@
import typing
import struct
import re
import zlib
import numpy as np
@ -76,6 +77,14 @@ class ImageData:
return None
return self.channels[3]
def get_extension(self):
allowed_extensions = ['.png', '.jpg', '.jpeg']
fallback_extension = allowed_extensions[0]
matches = re.findall(r'\.\w+$', self.filepath)
extension = matches[0] if len(matches) > 0 else fallback_extension
return extension if extension.lower() in allowed_extensions else fallback_extension
def to_image_data(self, mime_type: str) -> bytes:
if mime_type == 'image/png':
return self.to_png_data()