[Bf-extensions-cvs] [c0a678d3] master: Node Wrangler: improve Add Principled Setup file matching logic

Johan Walles noreply at git.blender.org
Thu Jan 19 17:23:09 CET 2023


Commit: c0a678d3686a591eb3041cc72b60aec2857d389a
Author: Johan Walles
Date:   Thu Jan 19 16:58:39 2023 +0100
Branches: master
https://developer.blender.org/rBAc0a678d3686a591eb3041cc72b60aec2857d389a

Node Wrangler: improve Add Principled Setup file matching logic

Improve how "Add Principled Setup" decides which selected files should go into
which sockets.

The new tests verify that the new logic does the right thing with metallic
textures downloaded from here, plus some corner cases. None of these worked
before:
* https://poliigon.com
* https://ambientcg.com
* https://3dtextures.me
* https://polyhaven.com

Adding new tests for textures from other sites should be simple by just
extending the existing tests.

What the new code does:
* Converts file names into tag lists using `split_into_components()`
* Remove common prefixes from all tag lists
* Remove common suffixes from all tag lists
* Ignore files matching no socket tags (filters out README files and
  previews usually)
* Iterate ^ until nothing changes any more
* Do the same matching as before in `match_files_to_socket_names()`,
  but on the filtered tag lists

Other changes:
* `node_wrangler.py` was moved into a `node_wrangler/main.py` to enable putting
  tests next to it. Inspired by `io_curve_svg/` and its `svg_util_test.py`.
* File-names-to-socket-matching code was moved into its own file `util.py`
  so that both tests and the production code can find it
* Tests were added in `node_wrangler/util_test.py`

Differential Revision: https://developer.blender.org/D16940

===================================================================

R099	node_wrangler.py	node_wrangler/__init__.py
A	node_wrangler/util.py
A	node_wrangler/util_test.py

===================================================================

diff --git a/node_wrangler.py b/node_wrangler/__init__.py
similarity index 99%
rename from node_wrangler.py
rename to node_wrangler/__init__.py
index 3b9378ee..2de39b34 100644
--- a/node_wrangler.py
+++ b/node_wrangler/__init__.py
@@ -27,6 +27,7 @@ from bpy.props import (
 from bpy_extras.io_utils import ImportHelper, ExportHelper
 from gpu_extras.batch import batch_for_shader
 from mathutils import Vector
+from .util import match_files_to_socket_names, split_into_components
 from nodeitems_utils import node_categories_iter, NodeItemCustom
 from math import cos, sin, pi, hypot
 from os import path
@@ -679,7 +680,7 @@ def get_output_location(tree):
 class NWPrincipledPreferences(bpy.types.PropertyGroup):
     base_color: StringProperty(
         name='Base Color',
-        default='diffuse diff albedo base col color',
+        default='diffuse diff albedo base col color basecolor',
         description='Naming Components for Base Color maps')
     sss_color: StringProperty(
         name='Subsurface Color',
@@ -2712,25 +2713,6 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
             self.report({'INFO'}, 'Select Principled BSDF')
             return {'CANCELLED'}
 
-        # Helper_functions
-        def split_into__components(fname):
-            # Split filename into components
-            # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
-            # Remove extension
-            fname = path.splitext(fname)[0]
-            # Remove digits
-            fname = ''.join(i for i in fname if not i.isdigit())
-            # Separate CamelCase by space
-            fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
-            # Replace common separators with SPACE
-            separators = ['_', '.', '-', '__', '--', '#']
-            for sep in separators:
-                fname = fname.replace(sep, ' ')
-
-            components = fname.split(' ')
-            components = [c.lower() for c in components]
-            return components
-
         # Filter textures names for texturetypes in filenames
         # [Socket Name, [abbreviations and keyword list], Filename placeholder]
         tags = context.preferences.addons[__name__].preferences.principled_tags
@@ -2752,19 +2734,7 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
         ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
         ]
 
-        # Look through texture_types and set value as filename of first matched file
-        def match_files_to_socket_names():
-            for sname in socketnames:
-                for file in self.files:
-                    fname = file.name
-                    filenamecomponents = split_into__components(fname)
-                    matches = set(sname[1]).intersection(set(filenamecomponents))
-                    # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
-                    if matches:
-                        sname[2] = fname
-                        break
-
-        match_files_to_socket_names()
+        match_files_to_socket_names(self.files, socketnames)
         # Remove socketnames without found files
         socketnames = [s for s in socketnames if s[2]
                        and path.exists(self.directory+s[2])]
@@ -2838,7 +2808,7 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
                 # NORMAL NODES
                 if sname[0] == 'Normal':
                     # Test if new texture node is normal or bump map
-                    fname_components = split_into__components(sname[2])
+                    fname_components = split_into_components(sname[2])
                     match_normal = set(normal_abbr).intersection(set(fname_components))
                     match_bump = set(bump_abbr).intersection(set(fname_components))
                     if match_normal:
@@ -2855,7 +2825,7 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
 
                 elif sname[0] == 'Roughness':
                     # Test if glossy or roughness map
-                    fname_components = split_into__components(sname[2])
+                    fname_components = split_into_components(sname[2])
                     match_rough = set(rough_abbr).intersection(set(fname_components))
                     match_gloss = set(gloss_abbr).intersection(set(fname_components))
 
diff --git a/node_wrangler/util.py b/node_wrangler/util.py
new file mode 100644
index 00000000..80ca0bed
--- /dev/null
+++ b/node_wrangler/util.py
@@ -0,0 +1,164 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from os import path
+import re
+
+
+def split_into_components(fname):
+    """
+    Split filename into components
+    'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
+    """
+    # Remove extension
+    fname = path.splitext(fname)[0]
+    # Remove digits
+    fname = "".join(i for i in fname if not i.isdigit())
+    # Separate CamelCase by space
+    fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>", fname)
+    # Replace common separators with SPACE
+    separators = ["_", ".", "-", "__", "--", "#"]
+    for sep in separators:
+        fname = fname.replace(sep, " ")
+
+    components = fname.split(" ")
+    components = [c.lower() for c in components]
+    return components
+
+
+def remove_common_prefix(names_to_tag_lists):
+    """
+    Accepts a mapping of file names to tag lists that should be used for socket
+    matching.
+
+    This function modifies the provided mapping so that any common prefix
+    between all the tag lists is removed.
+
+    Returns true if some prefix was removed, false otherwise.
+    """
+    if not names_to_tag_lists:
+        return False
+    sample_tags = next(iter(names_to_tag_lists.values()))
+    if not sample_tags:
+        return False
+
+    common_prefix = sample_tags[0]
+    for tag_list in names_to_tag_lists.values():
+        if tag_list[0] != common_prefix:
+            return False
+
+    for name, tag_list in names_to_tag_lists.items():
+        names_to_tag_lists[name] = tag_list[1:]
+    return True
+
+
+def remove_common_suffix(names_to_tag_lists):
+    """
+    Accepts a mapping of file names to tag lists that should be used for socket
+    matching.
+
+    This function modifies the provided mapping so that any common suffix
+    between all the tag lists is removed.
+
+    Returns true if some suffix was removed, false otherwise.
+    """
+    if not names_to_tag_lists:
+        return False
+    sample_tags = next(iter(names_to_tag_lists.values()))
+    if not sample_tags:
+        return False
+
+    common_suffix = sample_tags[-1]
+    for tag_list in names_to_tag_lists.values():
+        if tag_list[-1] != common_suffix:
+            return False
+
+    for name, tag_list in names_to_tag_lists.items():
+        names_to_tag_lists[name] = tag_list[:-1]
+    return True
+
+
+def files_to_clean_file_names_for_sockets(files, sockets):
+    """
+    Accepts a list of files and a list of sockets.
+
+    Returns a mapping from file names to tag lists that should be used for
+    classification.
+
+    A file is something that we can do x.name on to figure out the file name.
+
+    A socket is a tuple containing:
+    * name
+    * list of tags
+    * a None field where the selected file name will go later. Ignored by us.
+    """
+
+    names_to_tag_lists = {}
+    for file in files:
+        names_to_tag_lists[file.name] = split_into_components(file.name)
+
+    all_tags = set()
+    for socket in sockets:
+        socket_tags = socket[1]
+        all_tags.update(socket_tags)
+
+    while True:
+        something_changed = False
+
+        # Common prefixes / suffixes provide zero information about what file
+        # should go to which socket, but they can confuse the mapping. So we get
+        # rid of them here.
+        something_changed |= remove_common_prefix(names_to_tag_lists)
+        something_changed |= remove_common_suffix(names_to_tag_lists)
+
+        # Names matching zero tags provide no value, remove those
+        names_to_remove = set()
+        for name, tag_list in names_to_tag_lists.items():
+            match_found = False
+            for tag in tag_list:
+                if tag in all_tags:
+                    match_found = True
+
+            if not match_found:
+                names_to_remove.add(name)
+
+        for name_to_remove in names_to_remove:
+            del names_to_tag_lists[name_to_remove]
+            something_changed = True
+
+        if not something_changed:
+            break
+
+    return names_to_tag_lists
+
+
+def match_files_to_socket_names(files, sockets):
+    """
+    Given a list of files and a list of sockets, match file names to sockets.
+
+    A file is something that you can get a file name out of using x.name.
+
+    After this function returns, all possible sockets have had their file names
+    filled in. Sockets without any matches will not get their file names
+    changed.
+
+    Sockets list format. Note that all file names are initially expected to be
+    None. Tags are strings, as are the socket names: [
+        [
+            socket_name, [tags], Optional[file_name]
+        ]
+    ]
+    """
+
+    names_to_tag_lists = files_to_clean_file_names_for_sockets(files, sockets)
+
+    for sname in sockets:
+        for name, tag_list in names_to_tag_lists.items():
+            if sname[0] == "Normal" and "dx" in tag_list:
+                # Blender wants GL normals, not DX (DirectX) ones:
+                # https://www.reddit.com/r/blender/comments/rbuaua/texture_contains_normaldx_and_normalgl_files/
+                continue
+
+            matches = set(sname[1]).intersection(set(tag_list))
+            if matches:
+                sname[2] = name
+                break
diff --git a/node_wrangler/util_test.py b/node_wrangler/util_test.py
new file mode 100755
index 00000000..210259ae
--- /dev/null
+++ b/node_wrangler/util_test.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# pylint: disable=missing-function-docstring
+# pylint: disable=missing-class-docstring
+
+import unittest
+from dataclasses import dataclass
+
+# X

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-extensions-cvs mailing list