- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`. - Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`. - Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration. - Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency. - Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments. - Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
201 lines
12 KiB
Python
201 lines
12 KiB
Python
"""Build a quad panorama with periodic sampling and radial depth separation."""
|
|
|
|
import math
|
|
|
|
import bpy
|
|
|
|
from ..common.validity import delete_invalid_points
|
|
|
|
from ..common.depth_surface import (
|
|
_math, split_depth_surface, build_surface_camera, CUT_ATTRIBUTE,
|
|
)
|
|
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field, remove_attributes
|
|
from ..common.material_slots import inherit_material_slots
|
|
from ..common.smoothing import expand_smooth
|
|
from ..common.boundary_smoothing import smooth_boundary_depth, smooth_cut_boundary
|
|
|
|
|
|
def _quad_sphere(group, segments):
|
|
nodes, links = group.nodes, group.links
|
|
cube = nodes.new("GeometryNodeMeshCube")
|
|
cube.inputs["Size"].default_value = (2, 2, 2)
|
|
for axis in "XYZ":
|
|
links.new(segments, cube.inputs[f"Vertices {axis}"])
|
|
xyz = nodes.new("ShaderNodeSeparateXYZ")
|
|
links.new(nodes.new("GeometryNodeInputPosition").outputs[0], xyz.inputs[0])
|
|
squares = {axis: _math(group, "MULTIPLY", xyz.outputs[axis], xyz.outputs[axis]) for axis in "XYZ"}
|
|
combine = nodes.new("ShaderNodeCombineXYZ")
|
|
for axis, a, b in (("X", "Y", "Z"), ("Y", "Z", "X"), ("Z", "X", "Y")):
|
|
half = _math(group, "DIVIDE", _math(group, "ADD", squares[a], squares[b]), 2)
|
|
third = _math(group, "DIVIDE", _math(group, "MULTIPLY", squares[a], squares[b]), 3)
|
|
root = _math(group, "SQRT", _math(group, "ADD", _math(group, "SUBTRACT", 1, half), third))
|
|
links.new(_math(group, "MULTIPLY", xyz.outputs[axis], root), combine.inputs[axis])
|
|
position = nodes.new("GeometryNodeSetPosition")
|
|
links.new(cube.outputs["Mesh"], position.inputs["Geometry"])
|
|
links.new(combine.outputs[0], position.inputs["Position"])
|
|
return position.outputs["Geometry"]
|
|
|
|
|
|
def build_image_depth_panorama_group():
|
|
group = bpy.data.node_groups.new("O Image Depth Panorama", "GeometryNodeTree")
|
|
group.is_modifier = group.use_fake_user = True
|
|
group["anyimage_asset_revision"] = 15
|
|
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
|
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
|
|
subdivide.default_value, subdivide.min_value, subdivide.max_value = 8, 0, 8
|
|
float_input(group, "Depth Scale", 1, maximum=1, subtype="FACTOR").description = "Blend between a sphere at zero and the estimated depth surface at one."
|
|
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Higher values detect smaller depth jumps. Zero disables splitting."
|
|
smooth = interface_socket(group, "Smooth", "INPUT", "NodeSocketInt")
|
|
smooth.description = "Set the number of mesh smoothing passes. Higher values smooth the surface more but may soften details. Zero disables smoothing."
|
|
smooth.default_value, smooth.min_value, smooth.max_value = 0, 0, 20
|
|
valid_only = interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
|
|
valid_only.default_value = True
|
|
valid_only.description = "Remove mesh areas marked invalid by the depth mask."
|
|
group.interface.move(valid_only, smooth.position)
|
|
options = group.interface.new_panel("Options", default_closed=True)
|
|
boundary_smooth = interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", options)
|
|
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 2, 0, 20
|
|
boundary_smooth.description = "Smooth depth and geometry near cut edges on the panorama surface. Zero disables boundary smoothing."
|
|
float_input(group, "Smooth Weight", 1, maximum=1, subtype="FACTOR", parent=options).description = "Control the strength of each mesh smoothing pass. Higher values move vertices more."
|
|
float_input(group, "Mask Threshold", 0.9, maximum=1, subtype="FACTOR", parent=options).description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
|
float_input(group, "Invalid Distance", 100, minimum=1e-5, subtype="DISTANCE", parent=options).description = "Set the fallback distance from the panorama center for invalid depth areas kept when Depth Mask is disabled."
|
|
data = group.interface.new_panel("Data", default_closed=True)
|
|
interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
|
|
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
nodes, links = group.nodes, group.links
|
|
|
|
def uv(direction):
|
|
normalize = nodes.new("ShaderNodeVectorMath")
|
|
normalize.operation = "NORMALIZE"
|
|
links.new(direction, normalize.inputs[0])
|
|
xyz = nodes.new("ShaderNodeSeparateXYZ")
|
|
links.new(normalize.outputs[0], xyz.inputs[0])
|
|
longitude = _math(group, "DIVIDE", _math(group, "ARCTAN2", xyz.outputs["Y"], xyz.outputs["X"]), 2 * math.pi)
|
|
z = _math(group, "MAXIMUM", -1, _math(group, "MINIMUM", 1, xyz.outputs["Z"]))
|
|
latitude = _math(group, "SUBTRACT", 1, _math(group, "DIVIDE", _math(group, "ARCCOSINE", z), math.pi))
|
|
return longitude, latitude, xyz.outputs["Z"]
|
|
|
|
def combine(u, v):
|
|
node = nodes.new("ShaderNodeCombineXYZ")
|
|
links.new(u, node.inputs["X"])
|
|
links.new(v, node.inputs["Y"])
|
|
return node.outputs[0]
|
|
|
|
def texture(coordinates, interpolation):
|
|
inputs = group_input(nodes, {"Depth Image"})
|
|
info = nodes.new("GeometryNodeImageInfo")
|
|
links.new(inputs.outputs["Depth Image"], info.inputs["Image"])
|
|
axes = nodes.new("ShaderNodeSeparateXYZ")
|
|
links.new(coordinates, axes.inputs[0])
|
|
half_pixel = _math(group, "DIVIDE", 0.5, info.outputs["Height"])
|
|
v = _math(group, "MAXIMUM", half_pixel,
|
|
_math(group, "MINIMUM", axes.outputs["Y"], _math(group, "SUBTRACT", 1, half_pixel)))
|
|
node = nodes.new("GeometryNodeImageTexture")
|
|
node.interpolation, node.extension = interpolation, "REPEAT"
|
|
links.new(inputs.outputs["Depth Image"], node.inputs["Image"])
|
|
links.new(combine(axes.outputs["X"], v), node.inputs["Vector"])
|
|
return node.outputs["Color"]
|
|
|
|
def depth(coordinates):
|
|
linear, nearest = texture(coordinates, "Linear"), texture(coordinates, "Closest")
|
|
alphas = []
|
|
for color in (linear, nearest):
|
|
separate = nodes.new("FunctionNodeSeparateColor")
|
|
links.new(color, separate.inputs["Color"])
|
|
alphas.append(separate.outputs["Alpha"])
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "RGBA"
|
|
links.new(_math(group, "LESS_THAN", alphas[0],
|
|
_math(group, "SUBTRACT", alphas[1], 1e-6)), choose.inputs["Switch"])
|
|
links.new(linear, choose.inputs["False"])
|
|
links.new(nearest, choose.inputs["True"])
|
|
return choose.outputs[0], alphas[1]
|
|
|
|
inputs = group_input(nodes, {"Subdivide"})
|
|
sphere = _quad_sphere(group, _math(group, "ADD", _math(group, "POWER", 2, inputs.outputs["Subdivide"]), 1))
|
|
position = nodes.new("GeometryNodeInputPosition").outputs[0]
|
|
longitude, latitude, z = uv(position)
|
|
coordinates = evaluate_field(group, combine(longitude, latitude), "FLOAT_VECTOR", "POINT")
|
|
face = nodes.new("GeometryNodeFieldOnDomain")
|
|
face.data_type, face.domain = "FLOAT_VECTOR", "FACE"
|
|
links.new(position, face.inputs["Value"])
|
|
face_u, face_v, _ = uv(face.outputs["Value"])
|
|
correction = _math(group, "ROUND", _math(group, "SUBTRACT", face_u, longitude))
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "FLOAT"
|
|
links.new(_math(group, "GREATER_THAN", _math(group, "ABSOLUTE", z), 0.999999), choose.inputs["Switch"])
|
|
links.new(_math(group, "ADD", longitude, correction), choose.inputs["False"])
|
|
links.new(face_u, choose.inputs["True"])
|
|
uv_map = nodes.new("GeometryNodeStoreNamedAttribute")
|
|
uv_map.data_type, uv_map.domain = "FLOAT2", "CORNER"
|
|
uv_map.inputs["Name"].default_value = "UVMap"
|
|
links.new(sphere, uv_map.inputs["Geometry"])
|
|
links.new(combine(choose.outputs[0], latitude), uv_map.inputs["Value"])
|
|
pixel, _ = depth(coordinates)
|
|
pixel = evaluate_field(group, pixel, "FLOAT_VECTOR", "POINT")
|
|
nearest = nodes.new("FunctionNodeSeparateColor")
|
|
links.new(texture(coordinates, "Closest"), nearest.inputs["Color"])
|
|
controls = group_input(nodes, {"Mask Threshold"})
|
|
invalid = _math(group, "MAXIMUM",
|
|
_math(group, "LESS_THAN", nearest.outputs["Alpha"], controls.outputs["Mask Threshold"]),
|
|
_math(group, "LESS_THAN", nearest.outputs["Red"], 1e-5))
|
|
face_invalid = evaluate_field(group, invalid, "FLOAT", "POINT")
|
|
any_invalid = _math(group, "GREATER_THAN", face_invalid, 0)
|
|
mixed = _math(group, "MULTIPLY", any_invalid,
|
|
_math(group, "LESS_THAN", face_invalid, 1))
|
|
protected = uv_map.outputs["Geometry"]
|
|
culled = delete_invalid_points(group, protected, any_invalid)
|
|
delete = nodes.new("GeometryNodeDeleteGeometry")
|
|
delete.domain, delete.mode = "FACE", "ALL"
|
|
links.new(protected, delete.inputs["Geometry"])
|
|
links.new(mixed, delete.inputs["Selection"])
|
|
selection = nodes.new("GeometryNodeSwitch")
|
|
selection.input_type = "GEOMETRY"
|
|
links.new(group_input(nodes, {"Depth Mask"}).outputs["Depth Mask"], selection.inputs["Switch"])
|
|
links.new(delete.outputs[0], selection.inputs["False"])
|
|
links.new(culled, selection.inputs["True"])
|
|
controls = group_input(nodes, {"Depth Split"})
|
|
strength = controls.outputs["Depth Split"]
|
|
face_camera = depth(combine(face_u, face_v))[0]
|
|
radial = nodes.new("ShaderNodeSeparateXYZ")
|
|
links.new(face_camera, radial.inputs[0])
|
|
geometry, corner_camera, cut_vertex = split_depth_surface(
|
|
group, selection.outputs[0], face_camera, strength, 1.0,
|
|
face_distance=radial.outputs["X"], face_center=face.outputs["Value"],
|
|
)
|
|
distance = build_surface_camera(group, pixel, corner_camera, cut_vertex, strength)
|
|
axes = nodes.new("ShaderNodeSeparateXYZ")
|
|
links.new(distance, axes.inputs[0])
|
|
distance = nodes.new("GeometryNodeSwitch")
|
|
distance.input_type = "FLOAT"
|
|
links.new(face_invalid, distance.inputs["Switch"])
|
|
links.new(smooth_boundary_depth(group, axes.outputs["X"], face_invalid), distance.inputs["False"])
|
|
links.new(group_input(nodes, {"Invalid Distance"}).outputs["Invalid Distance"], distance.inputs["True"])
|
|
controls = group_input(nodes, {"Depth Scale"})
|
|
radius = _math(group, "ADD", 1, _math(group, "MULTIPLY", controls.outputs["Depth Scale"], _math(group, "SUBTRACT", distance.outputs[0], 1)))
|
|
radial = nodes.new("ShaderNodeVectorMath")
|
|
radial.operation = "SCALE"
|
|
links.new(position, radial.inputs[0])
|
|
links.new(radius, radial.inputs[3])
|
|
deform = nodes.new("GeometryNodeSetPosition")
|
|
links.new(geometry, deform.inputs["Geometry"])
|
|
links.new(radial.outputs[0], deform.inputs["Position"])
|
|
geometry = remove_attributes(
|
|
group, deform.outputs[0], CUT_ATTRIBUTE,
|
|
enabled=_math(group, "GREATER_THAN", strength, 0.0),
|
|
)
|
|
geometry = smooth_cut_boundary(group, geometry)
|
|
geometry = expand_smooth(group, geometry, iterations_name="Smooth", weight_name="Smooth Weight")
|
|
flip = nodes.new("GeometryNodeFlipFaces")
|
|
links.new(geometry, flip.inputs["Mesh"])
|
|
smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
|
smooth.domain = "FACE"
|
|
smooth.inputs["Shade Smooth"].default_value = True
|
|
links.new(flip.outputs["Mesh"], smooth.inputs["Geometry"])
|
|
geometry = inherit_material_slots(group, smooth.outputs[0], group_input(nodes, {"Geometry"}).outputs["Geometry"])
|
|
output = nodes.new("NodeGroupOutput")
|
|
links.new(geometry, output.inputs["Geometry"])
|
|
prepare_node_group(group)
|
|
return group
|