feat(cutout): implement boundary padding for cutout textures
- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px. - Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth. - Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data. - Added tests to validate the new boundary padding functionality and its integration with existing cutout processes. feat(geometry): remove overall mesh smoothing from geometry nodes - Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing. - Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes. - Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent. - Updated tests and documentation to reflect the changes in node interfaces and behaviors. feat(normals): scale tangent normals with depth - Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`. - Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute. - Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
This commit is contained in:
@@ -1,10 +1,45 @@
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class RecordingLayout:
|
||||
"""Minimal Blender layout that records the properties and operators drawn."""
|
||||
|
||||
enabled = True
|
||||
|
||||
def __init__(self, operators):
|
||||
self.properties = []
|
||||
self.operators = operators
|
||||
|
||||
def prop(self, _owner, name, **_options):
|
||||
self.properties.append(name)
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operator = SimpleNamespace(model="")
|
||||
self.operators.append((identifier, operator))
|
||||
return operator
|
||||
|
||||
def box(self):
|
||||
return self
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def split(self, **_options):
|
||||
return self
|
||||
|
||||
def column(self, **_options):
|
||||
return self
|
||||
|
||||
def label(self, **_options):
|
||||
return None
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
|
||||
class PreferencesTest(BlenderTestCase):
|
||||
def test_apple_silicon_architectures_support_coreml(self):
|
||||
properties = self.anyimage.properties
|
||||
@@ -107,93 +142,30 @@ class PreferencesTest(BlenderTestCase):
|
||||
setattr(stored, attribute, "UNKNOWN")
|
||||
self.assertEqual(reader(), default)
|
||||
self.assertEqual(getattr(stored, attribute), default)
|
||||
self.assertEqual({item[0]: item[-1] for item in module.BACKGROUND_MODEL_ITEMS}["BEN2_BASE"], 0)
|
||||
self.assertEqual({item[0]: item[-1] for item in module.UPSCALE_MODEL_ITEMS}["REALESRGAN_X4PLUS"], 2)
|
||||
self.assertEqual({item[0]: item[-1] for item in self.anyimage.properties.geometry_model_enum_items(None, None)}["MOGE3_VITL"], 3)
|
||||
|
||||
|
||||
def test_global_ai_input_size_defaults_and_clamps(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
2048,
|
||||
)
|
||||
preferences.max_ai_input_size = 10000
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
8192,
|
||||
)
|
||||
preferences.max_ai_input_size = 1
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
256,
|
||||
)
|
||||
|
||||
|
||||
def test_cutout_relative_edge_length_defaults_and_clamps(self):
|
||||
def test_numeric_preferences_clamp_to_supported_ranges(self):
|
||||
module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.01)
|
||||
preferences.min_cutout_relative_edge_length = 2
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.02)
|
||||
preferences.min_cutout_relative_edge_length = 200
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 1)
|
||||
preferences.min_cutout_relative_edge_length = 0
|
||||
self.assertAlmostEqual(module.configured_min_cutout_relative_edge_length(), 0.00001)
|
||||
cases = (
|
||||
(module.configured_max_ai_input_size, "max_ai_input_size", (10000, 1), (8192, 256)),
|
||||
(module.configured_max_frame_resolution, "max_frame_resolution", (10000, 1), (8192, 64)),
|
||||
(module.configured_cutout_boundary_padding, "cutout_boundary_padding", (3.5, 100, -1), (3.5, 4, 1)),
|
||||
(
|
||||
module.configured_min_cutout_relative_edge_length,
|
||||
"min_cutout_relative_edge_length",
|
||||
(2, 200, 0),
|
||||
(0.02, 1, 0.00001),
|
||||
),
|
||||
)
|
||||
for reader, attribute, values, expected in cases:
|
||||
with self.subTest(attribute=attribute):
|
||||
for value, result in zip(values, expected):
|
||||
setattr(preferences, attribute, value)
|
||||
self.assertAlmostEqual(reader(), result)
|
||||
|
||||
|
||||
def test_frame_resolution_defaults_and_clamps_independently(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 2048)
|
||||
preferences.max_frame_resolution = 10000
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 8192)
|
||||
preferences.max_frame_resolution = 1
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 64)
|
||||
|
||||
|
||||
def test_preferences_draws_upscale_model_download(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
downloads = []
|
||||
|
||||
class Layout:
|
||||
enabled = True
|
||||
|
||||
def prop(self, _owner, name, **_options):
|
||||
pass
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
def label(self, **_options):
|
||||
pass
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def split(self, **_options):
|
||||
return self
|
||||
|
||||
def column(self, **_options):
|
||||
return self
|
||||
|
||||
def box(self):
|
||||
return self
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operator = SimpleNamespace(model="")
|
||||
if identifier == "anyimage.download_model":
|
||||
downloads.append(operator)
|
||||
return operator
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
preferences.geometry_model = "MOGE2_VITS_NORMAL"
|
||||
preferences.background_model = "BEN2_BASE"
|
||||
preferences.upscale_model = "HAT_GAN_X4_SHARPER"
|
||||
def test_preferences_draw_follows_ai_status(self):
|
||||
catalog = tuple(
|
||||
{
|
||||
"key": key,
|
||||
@@ -205,82 +177,46 @@ class PreferencesTest(BlenderTestCase):
|
||||
for key, family in (
|
||||
("MOGE2_VITS_NORMAL", "moge"),
|
||||
("BEN2_BASE", "ben2"),
|
||||
("REALESRGAN_GENERAL_WDN_X4V3", "upscale"),
|
||||
("HAT_GAN_X4_SHARPER", "upscale"),
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"ai_status",
|
||||
return_value={
|
||||
"environment_ready": True,
|
||||
"missing_models": (),
|
||||
"ready": True,
|
||||
},
|
||||
),
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"model_catalog",
|
||||
return_value=catalog,
|
||||
),
|
||||
patch.object(self.anyimage.runtime, "server_busy", return_value=False),
|
||||
):
|
||||
preferences.draw(None)
|
||||
|
||||
missing_environment = {
|
||||
"environment_ready": False,
|
||||
"missing_models": (),
|
||||
"ready": False,
|
||||
}
|
||||
operators = self.draw_preferences(missing_environment, ())
|
||||
self.assertEqual(
|
||||
[download.model for download in downloads],
|
||||
[identifier for identifier, _options in operators],
|
||||
["anyimage.setup_ai_environment", "anyimage.open_server_log"],
|
||||
)
|
||||
|
||||
ready = {"environment_ready": True, "missing_models": (), "ready": True}
|
||||
operators = self.draw_preferences(ready, catalog)
|
||||
downloads = [
|
||||
operator.model
|
||||
for identifier, operator in operators
|
||||
if identifier == "anyimage.download_model"
|
||||
]
|
||||
self.assertEqual(
|
||||
downloads,
|
||||
["MOGE2_VITS_NORMAL", "BEN2_BASE", "HAT_GAN_X4_SHARPER"],
|
||||
)
|
||||
|
||||
|
||||
def test_preferences_hide_ai_until_server_is_installed(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
def draw_preferences(self, status, catalog):
|
||||
operators = []
|
||||
|
||||
class Layout:
|
||||
enabled = True
|
||||
|
||||
def box(self):
|
||||
return self
|
||||
|
||||
def label(self, **options):
|
||||
pass
|
||||
|
||||
def prop(self, *_args, **_options):
|
||||
return None
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operators.append(identifier)
|
||||
return SimpleNamespace(model="")
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
preferences = self.anyimage.preferences.AnyImagePreferences()
|
||||
preferences.layout = RecordingLayout(operators)
|
||||
preferences.geometry_model = "MOGE2_VITS_NORMAL"
|
||||
preferences.background_model = "BEN2_BASE"
|
||||
preferences.upscale_model = "HAT_GAN_X4_SHARPER"
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"ai_status",
|
||||
return_value={
|
||||
"environment_ready": False,
|
||||
"missing_models": (),
|
||||
"ready": False,
|
||||
},
|
||||
),
|
||||
patch.object(self.anyimage.properties, "model_catalog", return_value=()),
|
||||
patch.object(self.anyimage.properties, "ai_status", return_value=status),
|
||||
patch.object(self.anyimage.properties, "model_catalog", return_value=catalog),
|
||||
patch.object(self.anyimage.runtime, "server_busy", return_value=False),
|
||||
):
|
||||
preferences.draw(None)
|
||||
|
||||
self.assertEqual(
|
||||
operators,
|
||||
["anyimage.setup_ai_environment", "anyimage.open_server_log"],
|
||||
)
|
||||
return operators
|
||||
|
||||
|
||||
def test_depth_plane_request_includes_the_global_ai_input_limit(self):
|
||||
|
||||
Reference in New Issue
Block a user