glTF exporter: roundtrip all texture wrap modes

This commit is contained in:
Julien Duroure 2021-01-04 20:34:41 +01:00
parent 2660631657
commit 70efc485ec
4 changed files with 121 additions and 55 deletions

View File

@ -15,7 +15,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (1, 5, 9),
"version": (1, 5, 10),
'blender': (2, 91, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -17,18 +17,25 @@ from io_scene_gltf2.io.com import gltf2_io
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
from io_scene_gltf2.io.com.gltf2_io_constants import TextureFilter, TextureWrap
from io_scene_gltf2.blender.exp.gltf2_blender_get import (
previous_node,
previous_socket,
get_const_from_socket,
)
@cached
def gather_sampler(blender_shader_node: bpy.types.Node, export_settings):
wrap_s, wrap_t = __gather_wrap(blender_shader_node, export_settings)
sampler = gltf2_io.Sampler(
extensions=__gather_extensions(blender_shader_node, export_settings),
extras=__gather_extras(blender_shader_node, export_settings),
mag_filter=__gather_mag_filter(blender_shader_node, export_settings),
min_filter=__gather_min_filter(blender_shader_node, export_settings),
name=__gather_name(blender_shader_node, export_settings),
wrap_s=__gather_wrap_s(blender_shader_node, export_settings),
wrap_t=__gather_wrap_t(blender_shader_node, export_settings)
wrap_s=wrap_s,
wrap_t=wrap_t,
)
export_user_extensions('gather_sampler_hook', export_settings, sampler, blender_shader_node)
@ -83,13 +90,83 @@ def __gather_name(blender_shader_node, export_settings):
return None
def __gather_wrap_s(blender_shader_node, export_settings):
def __gather_wrap(blender_shader_node, export_settings):
# First gather from the Texture node
if blender_shader_node.extension == 'EXTEND':
return TextureWrap.ClampToEdge
return None
wrap_s = TextureWrap.ClampToEdge
elif blender_shader_node.extension == 'CLIP':
# Not possible in glTF, but ClampToEdge is closest
wrap_s = TextureWrap.ClampToEdge
else:
wrap_s = TextureWrap.Repeat
wrap_t = wrap_s
# Take manual wrapping into account
result = detect_manual_uv_wrapping(blender_shader_node)
if result:
if result['wrap_s'] is not None: wrap_s = result['wrap_s']
if result['wrap_t'] is not None: wrap_t = result['wrap_t']
# Omit if both are repeat
if (wrap_s, wrap_t) == (TextureWrap.Repeat, TextureWrap.Repeat):
wrap_s, wrap_t = None, None
return wrap_s, wrap_t
def __gather_wrap_t(blender_shader_node, export_settings):
if blender_shader_node.extension == 'EXTEND':
return TextureWrap.ClampToEdge
return None
def detect_manual_uv_wrapping(blender_shader_node):
# Detects UV wrapping done using math nodes. This is for emulating wrap
# modes Blender doesn't support. It looks like
#
# next_socket => [Sep XYZ] => [Wrap S] => [Comb XYZ] => blender_shader_node
# => [Wrap T] =>
#
# The [Wrap _] blocks are either math nodes (eg. PINGPONG for mirrored
# repeat), or can be omitted.
#
# Returns None if not detected. Otherwise a dict containing the wrap
# mode in each direction (or None), and next_socket.
result = {}
comb = previous_node(blender_shader_node.inputs['Vector'])
if comb is None or comb.type != 'COMBXYZ': return None
for soc in ['X', 'Y']:
node = previous_node(comb.inputs[soc])
if node is None: return None
if node.type == 'SEPXYZ':
# Passed through without change
wrap = None
prev_socket = previous_socket(comb.inputs[soc])
elif node.type == 'MATH':
# Math node applies a manual wrap
if (node.operation == 'PINGPONG' and
get_const_from_socket(node.inputs[1], kind='VALUE') == 1.0): # scale = 1
wrap = TextureWrap.MirroredRepeat
elif (node.operation == 'WRAP' and
get_const_from_socket(node.inputs[1], kind='VALUE') == 0.0 and # min = 0
get_const_from_socket(node.inputs[2], kind='VALUE') == 1.0): # max = 1
wrap = TextureWrap.Repeat
else:
return None
prev_socket = previous_socket(node.inputs[0])
else:
return None
if prev_socket is None: return None
prev_node = prev_socket.node
if prev_node.type != 'SEPXYZ': return None
# Make sure X goes to X, etc.
if prev_socket.name != soc: return None
# Make sure both attach to the same SeparateXYZ node
if soc == 'X':
sep = prev_node
else:
if sep != prev_node: return None
result['wrap_s' if soc == 'X' else 'wrap_t'] = wrap
result['next_socket'] = sep.inputs[0]
return result

View File

@ -19,6 +19,8 @@ from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
from io_scene_gltf2.blender.exp import gltf2_blender_get
from io_scene_gltf2.blender.exp.gltf2_blender_get import previous_node
from io_scene_gltf2.blender.exp.gltf2_blender_gather_sampler import detect_manual_uv_wrapping
from io_scene_gltf2.io.com.gltf2_io_extensions import Extension
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
@ -47,11 +49,13 @@ def __gather_texture_info_helper(
if not __filter_texture_info(primary_socket, blender_shader_sockets, export_settings):
return None
tex_transform, tex_coord = __gather_texture_transform_and_tex_coord(primary_socket, export_settings)
fields = {
'extensions': __gather_extensions(primary_socket, export_settings),
'extensions': __gather_extensions(tex_transform, export_settings),
'extras': __gather_extras(blender_shader_sockets, export_settings),
'index': __gather_index(blender_shader_sockets, export_settings),
'tex_coord': __gather_tex_coord(primary_socket, export_settings),
'tex_coord': tex_coord,
}
if kind == 'DEFAULT':
@ -89,17 +93,9 @@ def __filter_texture_info(primary_socket, blender_shader_sockets, export_setting
return True
def __gather_extensions(primary_socket, export_settings):
if not hasattr(primary_socket, 'links'):
return None
texture_node = __get_tex_from_socket(primary_socket).shader_node
if texture_node is None:
return None
texture_transform = gltf2_blender_get.get_texture_transform_from_texture_node(texture_node)
def __gather_extensions(texture_transform, export_settings):
if texture_transform is None:
return None
extension = Extension("KHR_texture_transform", texture_transform)
return {"KHR_texture_transform": extension}
@ -144,33 +140,37 @@ def __gather_index(blender_shader_sockets, export_settings):
return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets, export_settings)
def __gather_tex_coord(primary_socket, export_settings):
def __gather_texture_transform_and_tex_coord(primary_socket, export_settings):
# We're expecting
#
# [UV Map] => [Mapping] => [UV Wrapping] => [Texture Node] => ... => primary_socket
#
# The [UV Wrapping] is for wrap modes like MIRROR that use nodes,
# [Mapping] is for KHR_texture_transform, and [UV Map] is for texCoord.
blender_shader_node = __get_tex_from_socket(primary_socket).shader_node
if len(blender_shader_node.inputs['Vector'].links) == 0:
return 0
input_node = blender_shader_node.inputs['Vector'].links[0].from_node
# Skip over UV wrapping stuff (it goes in the sampler)
result = detect_manual_uv_wrapping(blender_shader_node)
if result:
node = previous_node(result['next_socket'])
else:
node = previous_node(blender_shader_node.inputs['Vector'])
if isinstance(input_node, bpy.types.ShaderNodeMapping):
texture_transform = None
if node and node.type == 'MAPPING':
texture_transform = gltf2_blender_get.get_texture_transform_from_mapping_node(node)
node = previous_node(node.inputs['Vector'])
if len(input_node.inputs['Vector'].links) == 0:
return 0
texcoord_idx = 0
if node and node.type == 'UVMAP' and node.uv_map:
# Try to gather map index.
for blender_mesh in bpy.data.meshes:
i = blender_mesh.uv_layers.find(node.uv_map)
if i >= 0:
texcoord_idx = i
break
input_node = input_node.inputs['Vector'].links[0].from_node
if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
return 0
if input_node.uv_map == '':
return 0
# Try to gather map index.
for blender_mesh in bpy.data.meshes:
texCoordIndex = blender_mesh.uv_layers.find(input_node.uv_map)
if texCoordIndex >= 0:
return texCoordIndex
return 0
return texture_transform, texcoord_idx or None
def __get_tex_from_socket(socket):

View File

@ -132,18 +132,7 @@ def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
return None
def get_texture_transform_from_texture_node(texture_node):
if not isinstance(texture_node, bpy.types.ShaderNodeTexImage):
return None
mapping_socket = texture_node.inputs["Vector"]
if len(mapping_socket.links) == 0:
return None
mapping_node = mapping_socket.links[0].from_node
if not isinstance(mapping_node, bpy.types.ShaderNodeMapping):
return None
def get_texture_transform_from_mapping_node(mapping_node):
if mapping_node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]:
gltf2_io_debug.print_console("WARNING",
"Skipping exporting texture transform because it had type " +