- Introduced `frames.py` to manage field complex calculations and their arrangement within node groups. - Added functionality to create and remove field frames based on node arrangements. - Developed `passages.py` to find unobstructed paths for connections around calculation frames. - Implemented `references.py` to route shared results on peripheral tracks, ensuring efficient distribution of outputs. - Created `sections.py` to reserve space for field calculations adjacent to geometry consumers. - Added `sources.py` to position shared calculation sources effectively beside their distribution channels. - Enhanced outline smoothing with `outline_smoothing.py` to adapt camera depth along original outlines.
105 lines
5.1 KiB
Python
105 lines
5.1 KiB
Python
"""Check adaptive outline batches against a numerical graph reference."""
|
|
|
|
from collections import Counter
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tests.support.depth_surface import evaluated
|
|
from tools.nodes.common.outline_smoothing import smooth_outline_camera
|
|
from tools.nodes.common.depth_surface import CUT_ATTRIBUTE
|
|
|
|
|
|
@pytest.mark.parametrize("steps", [1, 4])
|
|
def test_outline_batches_update_weights_preserve_split_and_camera_rays(steps):
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
points = np.array([(x, y, 0) for y in range(9) for x in range(9)], dtype=float)
|
|
faces = [(y*9+x, y*9+x+1, (y+1)*9+x+1, (y+1)*9+x) for y in range(8) for x in range(8)]
|
|
deleted = [i % 8 == 4 for i in range(len(faces))]
|
|
result_faces = [face for face, remove in zip(faces, deleted) if not remove]
|
|
cut = np.isin(points[:, 0], [4, 5])
|
|
depth = np.full(81, 2.0)
|
|
depth[2] = 20
|
|
depth[80] = 0
|
|
camera = np.column_stack((points[:, :2]*depth[:, None]/10, depth))
|
|
distance = np.linalg.norm(camera, axis=1)
|
|
mesh = bpy.data.meshes.new("Split outline")
|
|
mesh.from_pydata(points.tolist(), [], faces)
|
|
for name, kind, domain, values, prop in (
|
|
("test_camera", "FLOAT_VECTOR", "POINT", camera.ravel(), "vector"),
|
|
(CUT_ATTRIBUTE, "FLOAT", "POINT", cut, "value"),
|
|
("test_deleted", "BOOLEAN", "FACE", deleted, "value"),
|
|
):
|
|
mesh.attributes.new(name, kind, domain).data.foreach_set(prop, values)
|
|
obj = bpy.data.objects.new("Outline", mesh)
|
|
bpy.context.collection.objects.link(obj)
|
|
group = bpy.data.node_groups.new("Adaptive outline test", "GeometryNodeTree")
|
|
for direction in ("INPUT", "OUTPUT"):
|
|
group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry")
|
|
ids = {}
|
|
for name in ("Outline Depth Fix", "Boundary Smooth"):
|
|
ids[name] = group.interface.new_socket(name=name, in_out="INPUT", socket_type="NodeSocketInt").identifier
|
|
source = group.nodes.new("NodeGroupInput")
|
|
attributes = {}
|
|
for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_deleted", "BOOLEAN")):
|
|
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
|
node.data_type = kind
|
|
node.inputs["Name"].default_value = name
|
|
attributes[name] = node.outputs["Attribute"]
|
|
delete = group.nodes.new("GeometryNodeDeleteGeometry")
|
|
delete.domain, delete.mode = "FACE", "ONLY_FACE"
|
|
group.links.new(source.outputs["Geometry"], delete.inputs["Geometry"])
|
|
group.links.new(attributes["test_deleted"], delete.inputs["Selection"])
|
|
scale = group.nodes.new("ShaderNodeValue")
|
|
scale.outputs[0].default_value = 1
|
|
result = smooth_outline_camera(group, source.outputs["Geometry"], delete.outputs[0], attributes["test_camera"], scale.outputs[0])
|
|
position = group.nodes.new("GeometryNodeSetPosition")
|
|
group.links.new(delete.outputs[0], position.inputs["Geometry"])
|
|
group.links.new(result, position.inputs["Position"])
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
group.links.new(position.outputs[0], output.inputs["Geometry"])
|
|
modifier = obj.modifiers.new("Outline", "NODES")
|
|
modifier.node_group = group
|
|
|
|
# ONLY_FACE retains the separating column's edges in the current topology.
|
|
edges = np.array([tuple(e.vertices) for e in mesh.edges])
|
|
adjacency = np.zeros((81, 81))
|
|
adjacency[edges[:, 0], edges[:, 1]] = 1
|
|
adjacency += adjacency.T
|
|
degree = adjacency.sum(axis=1)
|
|
def blur(value, weight):
|
|
return (value + weight*(adjacency @ value))/(1 + weight*degree)
|
|
counts = Counter(tuple(sorted((a, b))) for f in result_faces for a, b in zip(f, (*f[1:], f[0])))
|
|
boundary = np.zeros(81)
|
|
for edge, count in counts.items():
|
|
if count == 1:
|
|
boundary[list(edge)] = 1
|
|
protected = boundary.astype(bool) & cut
|
|
seed = boundary*(~protected)
|
|
influence = np.maximum(seed, blur(blur(seed, 1), 1))*(~protected)
|
|
influence *= depth > 1e-8
|
|
for rounds, batch in ((0, steps), (16, 0), (16, steps)):
|
|
modifier[ids["Outline Depth Fix"]] = rounds
|
|
modifier[ids["Boundary Smooth"]] = batch
|
|
obj.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
actual, actual_faces = evaluated(obj)
|
|
expected = distance.copy()
|
|
for _ in range(rounds):
|
|
ratio = abs(expected-blur(expected, .5))
|
|
t = np.clip((ratio-.25)/.25, 0, 1)
|
|
weight = t*t*(3-2*t)*influence
|
|
for _ in range(batch):
|
|
expected = blur(expected, weight)
|
|
np.testing.assert_allclose(np.linalg.norm(actual, axis=1), expected, atol=2e-5)
|
|
assert actual_faces == result_faces
|
|
np.testing.assert_array_equal(actual[influence == 0], camera[influence == 0].astype(np.float32))
|
|
if rounds and batch:
|
|
assert 2 < actual[2, 2] < 15
|
|
valid = depth > 0
|
|
np.testing.assert_allclose(actual[valid, :2]/actual[valid, 2:3], camera[valid, :2]/camera[valid, 2:3], atol=1e-6)
|
|
else:
|
|
np.testing.assert_array_equal(actual, camera.astype(np.float32))
|
|
assert not any(n.bl_idname in {"GeometryNodeCaptureAttribute", "GeometryNodeStoreNamedAttribute"} for n in group.nodes)
|