- 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.
96 lines
4.4 KiB
Python
96 lines
4.4 KiB
Python
"""Reload saved assets and evaluate geometry in the target Blender process."""
|
|
from math import isfinite
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
|
|
ASSET_PATH = Path(__file__).resolve().parents[2] / "src/anyimage/assets/O_AnyImage.blend"
|
|
GEOMETRY_NAMES = {
|
|
"O Image Depth Panorama",
|
|
"O Image Plane", "O Image Depth Plane", "O Image Relief Plane",
|
|
"O Image Cutout", "O Image Depth Cutout", "O Image Cutout Symmetry",
|
|
}
|
|
SHADER_NAMES = {"O Image Layer", "O Image Depth Layer", "O Shadeless"}
|
|
|
|
|
|
def check_asset_library(asset_path):
|
|
"""Check library completeness and finite, nonempty evaluated geometry."""
|
|
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
|
|
assert set(source.node_groups) == GEOMETRY_NAMES | SHADER_NAMES
|
|
target.node_groups = sorted(GEOMETRY_NAMES | SHADER_NAMES)
|
|
pending, checked = list(target.node_groups), set()
|
|
while pending:
|
|
group = pending.pop()
|
|
if group in checked or group.bl_idname != "GeometryNodeTree":
|
|
continue
|
|
checked.add(group)
|
|
for node in group.nodes:
|
|
assert node.bl_idname != "GeometryNodeCaptureAttribute", group.name
|
|
if node.bl_idname == "GeometryNodeGroup" and node.node_tree:
|
|
pending.append(node.node_tree)
|
|
depth_image = bpy.data.images.new("Asset Check Depth", width=8, height=8, float_buffer=True)
|
|
depth_image.colorspace_settings.name = "Non-Color"
|
|
depth_image.pixels.foreach_set([
|
|
value for y in range(8) for x in range(8)
|
|
for value in ((x + 0.5) / 8 - 0.5, (y + 0.5) / 8 - 0.5, 2.0, 1.0)
|
|
])
|
|
for node_group in target.node_groups:
|
|
expected_type = "GeometryNodeTree" if node_group.name in GEOMETRY_NAMES else "ShaderNodeTree"
|
|
assert node_group.bl_idname == expected_type and node_group.library is not None, node_group.name
|
|
if expected_type == "ShaderNodeTree":
|
|
continue
|
|
mesh = bpy.data.meshes.new("Asset Check Plane")
|
|
mesh.from_pydata(
|
|
[(x / 3 - 0.5, y / 3 - 0.5, 0) for y in range(4) for x in range(4)], [],
|
|
[(y * 4 + x, y * 4 + x + 1, (y + 1) * 4 + x + 1, (y + 1) * 4 + x)
|
|
for y in range(3) for x in range(3)],
|
|
)
|
|
uv = mesh.uv_layers.new(name="UVMap")
|
|
for loop in mesh.loops:
|
|
position = mesh.vertices[loop.vertex_index].co
|
|
uv.data[loop.index].uv = (position.x + 0.5, position.y + 0.5)
|
|
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
|
|
for value in profile.data:
|
|
value.value = 0.2
|
|
obj = bpy.data.objects.new("Asset Check", mesh)
|
|
bpy.context.collection.objects.link(obj)
|
|
modifier = obj.modifiers.new("Asset Check", "NODES")
|
|
modifier.node_group = node_group
|
|
values = {"Subdivide": 0, "Thickness": 0.2, "Reference Depth": 2.0,
|
|
"Depth Image": depth_image, "Uniform Scale": 1.0, "Normal Smooth": 0,
|
|
"Depth Split": 0.0}
|
|
if node_group.name == "O Image Cutout Symmetry":
|
|
values["Offset"] = 0.5
|
|
for item in node_group.interface.items_tree:
|
|
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in values:
|
|
modifier[item.identifier] = values[item.name]
|
|
obj.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
result = evaluated.to_mesh()
|
|
try:
|
|
assert len(result.vertices) and len(result.polygons), node_group.name
|
|
assert all(isfinite(value) for vertex in result.vertices for value in vertex.co), node_group.name
|
|
assert not list(modifier.node_warnings), (
|
|
node_group.name, [warning.message for warning in modifier.node_warnings],
|
|
)
|
|
assert not {attribute.name for attribute in result.attributes} & {
|
|
".o_anyimage_depth_cut", ".o_anyimage_original_boundary",
|
|
}, node_group.name
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
bpy.data.meshes.remove(mesh)
|
|
bpy.data.images.remove(depth_image)
|
|
print(f"Checked {len(target.node_groups)} saved node groups in Blender {bpy.app.version_string}")
|
|
|
|
|
|
def main():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
check_asset_library(ASSET_PATH)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|