Add cutout profile and teddy balloon height calculations
- Implemented cotangent Poisson field and Poisson balloon profile in cutout_profile.py. - Added teddy balloon heights calculation and related functions in cutout_teddy.py. - Introduced interaction utilities for image editing in interaction.py. - Developed depth normalization and analysis functions in cutout_depth.py. - Created a validation script for cutout assets in validate_cutout_asset.py.
This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from anyimage.operators.balloon.depth import (
|
||||
BALLOON_DEPTH_ATTRIBUTE,
|
||||
add_balloon_shape_modifier,
|
||||
balloon_shape_node_group,
|
||||
)
|
||||
from anyimage.operators.balloon.operator import (
|
||||
mark_balloon_material,
|
||||
merge_balloon_normal_image,
|
||||
reusable_balloon_material,
|
||||
)
|
||||
from anyimage.operators.material import (
|
||||
BALLOON_MATERIAL_NODE_GROUP_NAME,
|
||||
material_node_group,
|
||||
set_modifier_input,
|
||||
)
|
||||
|
||||
|
||||
def input_socket(group, name):
|
||||
return next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == name
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = balloon_shape_node_group()
|
||||
assert group.is_modifier
|
||||
assert input_socket(group, "Double Sided Depth").default_value is False
|
||||
smoother = next(
|
||||
node
|
||||
for node in group.nodes
|
||||
if node.bl_idname == "ShaderNodeMapRange"
|
||||
and node.interpolation_type == "SMOOTHERSTEP"
|
||||
)
|
||||
assert smoother.clamp
|
||||
assert not any(
|
||||
node.bl_idname == "ShaderNodeMath"
|
||||
and node.operation == "MAXIMUM"
|
||||
and node.label == "Clamp Depth"
|
||||
for node in group.nodes
|
||||
)
|
||||
|
||||
mesh = bpy.data.meshes.new("Balloon Validation Mesh")
|
||||
mesh.from_pydata(
|
||||
(
|
||||
(-1.0, 0.0, 0.0),
|
||||
(-0.5, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0),
|
||||
(1.0, 0.0, 0.0),
|
||||
),
|
||||
(),
|
||||
(),
|
||||
)
|
||||
depth = mesh.attributes.new(BALLOON_DEPTH_ATTRIBUTE, "FLOAT", "POINT")
|
||||
depth.data.foreach_set("value", (0.0, 2.0, 2.0, 2.0))
|
||||
obj = bpy.data.objects.new("Balloon Validation", mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
modifier = add_balloon_shape_modifier(obj, 1.0)
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
x = [vertex.co.x for vertex in result.vertices]
|
||||
assert abs(x[0] + 1.0) < 1e-5
|
||||
assert abs(x[1] + 1.25) < 1e-5
|
||||
assert abs(x[2]) < 1e-5
|
||||
assert abs(x[3] - 1.0) < 1e-5
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
set_modifier_input(modifier, input_socket(group, "Thickness").identifier, 2.0)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
x = [vertex.co.x for vertex in result.vertices]
|
||||
assert abs(x[0] + 1.0) < 1e-5
|
||||
assert abs(x[3] - 2.0) < 1e-5
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
material_group = material_node_group(
|
||||
surface_group_name=BALLOON_MATERIAL_NODE_GROUP_NAME,
|
||||
)
|
||||
normal_maps = {
|
||||
node.space
|
||||
for node in material_group.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
}
|
||||
assert normal_maps == {"TANGENT", "OBJECT"}
|
||||
assert input_socket(material_group, "Object Space").default_value is False
|
||||
assert input_socket(material_group, "Normal Scale").default_value == 1.0
|
||||
|
||||
source_image = bpy.data.images.new("Reuse Source", 2, 1)
|
||||
shared_normal = bpy.data.images.new("Shared Normal", 2, 1)
|
||||
shared_normal.pixels.foreach_set(
|
||||
(128 / 255, 128 / 255, 1, 1, 128 / 255, 128 / 255, 1, 1)
|
||||
)
|
||||
material = bpy.data.materials.new("Unrelated Material Name")
|
||||
mark_balloon_material(material, source_image, shared_normal)
|
||||
assert reusable_balloon_material(
|
||||
source_image,
|
||||
normal_required=True,
|
||||
) is material
|
||||
|
||||
update_normal = bpy.data.images.new("Normal Update", 2, 1)
|
||||
update_normal.pixels.foreach_set(
|
||||
(1, 0.25, 0.75, 1, 128 / 255, 128 / 255, 1, 1)
|
||||
)
|
||||
merge_balloon_normal_image(shared_normal, update_normal)
|
||||
pixels = list(shared_normal.pixels)
|
||||
assert abs(pixels[0] - 1.0) < 1e-5
|
||||
assert abs(pixels[4] - 128 / 255) < 1e-5
|
||||
print(f"Validated Blender {bpy.app.version_string}: {group.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
165
tools/validate_cutout_asset.py
Normal file
165
tools/validate_cutout_asset.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from anyimage.operators import mesh
|
||||
from anyimage.operators.material import modifier_input_identifier, set_modifier_input
|
||||
|
||||
|
||||
def evaluated_mesh(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
return evaluated, result
|
||||
|
||||
|
||||
def grid_object(name):
|
||||
vertices = [
|
||||
(0.0, float(y), float(z))
|
||||
for z in range(3)
|
||||
for y in range(3)
|
||||
]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
a = z * 3 + y
|
||||
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
|
||||
data = bpy.data.meshes.new(name)
|
||||
data.from_pydata(vertices, (), faces)
|
||||
obj = bpy.data.objects.new(name, data)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def add_modifier(obj, shape, thickness):
|
||||
group = mesh._mesh_node_group(shape)
|
||||
modifier = obj.modifiers.new(group.name, "NODES")
|
||||
modifier.node_group = group
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(group, "Cleanup"),
|
||||
0.0,
|
||||
)
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(group, "Thickness"),
|
||||
thickness,
|
||||
)
|
||||
return modifier
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
groups = {
|
||||
shape: mesh._mesh_node_group(shape)
|
||||
for shape in (
|
||||
mesh.CUTOUT,
|
||||
mesh.BALLOON,
|
||||
mesh.DEPTH_BALLOON,
|
||||
mesh.DEPTH_SURFACE,
|
||||
)
|
||||
}
|
||||
assert [groups[shape].name for shape in groups] == [
|
||||
"O Image Surface",
|
||||
"O Image Balloon",
|
||||
"O Image Depth Balloon",
|
||||
"O Image Depth Surface",
|
||||
]
|
||||
repeat_counts = {
|
||||
shape: sum(
|
||||
node.bl_idname == "GeometryNodeRepeatInput"
|
||||
for node in group.nodes
|
||||
)
|
||||
for shape, group in groups.items()
|
||||
}
|
||||
assert repeat_counts == {
|
||||
mesh.CUTOUT: 1,
|
||||
mesh.BALLOON: 0,
|
||||
mesh.DEPTH_BALLOON: 0,
|
||||
mesh.DEPTH_SURFACE: 2,
|
||||
}
|
||||
for group in groups.values():
|
||||
assert group.is_modifier
|
||||
assert all(not node.label for node in group.nodes)
|
||||
assert all(
|
||||
link.from_node.location.x <= link.to_node.location.x
|
||||
for link in group.links
|
||||
)
|
||||
group_nodes = list(group.nodes)
|
||||
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
|
||||
for index, node in enumerate(group_nodes):
|
||||
for other in group_nodes[index + 1:]:
|
||||
assert not (
|
||||
abs(node.location.x - other.location.x) < 150.0
|
||||
and abs(node.location.y - other.location.y) < 100.0
|
||||
)
|
||||
for group_input in (
|
||||
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
|
||||
):
|
||||
assert all(
|
||||
output.is_linked or output.hide
|
||||
for output in group_input.outputs
|
||||
if output.name
|
||||
)
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT":
|
||||
assert item.structure_type == "SINGLE"
|
||||
|
||||
surface = grid_object("Surface Validation")
|
||||
add_modifier(surface, mesh.CUTOUT, 2.4)
|
||||
evaluated, result = evaluated_mesh(surface)
|
||||
try:
|
||||
assert len(result.vertices) == 34
|
||||
assert len(result.polygons) == 40
|
||||
side_faces = [
|
||||
polygon for polygon in result.polygons
|
||||
if abs(polygon.normal.x) < 0.1
|
||||
]
|
||||
assert len(side_faces) == 24
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
balloon = grid_object("Balloon Validation")
|
||||
profile = balloon.data.attributes.new(mesh.BALLOON_ATTRIBUTE, "FLOAT", "POINT")
|
||||
profile.data.foreach_set(
|
||||
"value",
|
||||
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
|
||||
)
|
||||
modifier = add_modifier(balloon, mesh.BALLOON, 1.0)
|
||||
evaluated, result = evaluated_mesh(balloon)
|
||||
try:
|
||||
initial_faces = len(result.polygons)
|
||||
initial_extent = max(abs(vertex.co.x) for vertex in result.vertices)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
2.0,
|
||||
)
|
||||
balloon.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated, result = evaluated_mesh(balloon)
|
||||
try:
|
||||
assert len(result.polygons) == initial_faces
|
||||
extent = max(abs(vertex.co.x) for vertex in result.vertices)
|
||||
assert abs(extent - initial_extent * 2.0) < 1e-5
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
print(
|
||||
f"Validated Blender {bpy.app.version_string}: "
|
||||
f"{', '.join(group.name for group in groups.values())}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -161,7 +161,7 @@ def main():
|
||||
static_depth = next(
|
||||
node.image
|
||||
for node in image_textures
|
||||
if node.image.name == "MoGe Test.depth"
|
||||
if node.image.name == "MoGe Test_depth.exr"
|
||||
)
|
||||
assert static_depth.source != "SEQUENCE"
|
||||
geometry_textures = [
|
||||
|
||||
Reference in New Issue
Block a user