Files
AnyImage/experiments/panorama_surface/quad_surface.py
nan 27ca25224a feat: Implement depth surface strip cleanup and organize tool CLI
- Added a proposal for depth surface strip cleanup, detailing the rationale and changes to the depth plane and cutout nodes.
- Created specifications for the new cleanup requirements, including scenarios for strip removal and boundary edge detection.
- Developed tasks for implementing shared strip detection logic and integrating new interfaces for depth nodes.
- Established a new CLI structure for organizing node groups and model commands, replacing old entry points with a more intuitive command structure.
- Introduced a prototype for panorama surface generation, including requirements for distance reconstruction and geometry node surfaces.
- Added comprehensive tests for depth strip cleanup functionality, ensuring correct behavior for various mesh topologies and edge cases.
2026-09-10 00:53:43 +08:00

152 lines
7.5 KiB
Python

"""Build an offline Quad Sphere panorama asset with point-domain validity culling."""
from pathlib import Path
import sys
import bpy
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from tools.nodes.common.nodes import interface_socket, prepare_node_group
REFERENCE_LIBRARY = Path("D:/Projects/OmooNodes/subspaces/O_Essentials/O_Essentials_GeometryNodes.blend1")
def build_node_group():
with bpy.data.libraries.load(str(REFERENCE_LIBRARY), link=False) as (source, target):
if "O Quad Sphere" not in source.node_groups:
raise ValueError("Reference library has no O Quad Sphere")
target.node_groups = ["O Quad Sphere"]
group = bpy.data.node_groups.new("Panorama Surface Trial", "GeometryNodeTree")
group.is_modifier = True
for name, kind in (("Subdivide", "NodeSocketInt"), ("Depth Scale", "NodeSocketFloat"),
("Validity Threshold", "NodeSocketFloat"),
("Distance", "NodeSocketImage"), ("Material", "NodeSocketMaterial")):
socket = interface_socket(group, name, "INPUT", kind)
if name == "Subdivide":
socket.default_value, socket.min_value, socket.max_value = 7, 0, 8
elif name == "Depth Scale":
socket.default_value, socket.min_value, socket.max_value = 1.0, 0.0, 1.0
socket.subtype = "FACTOR"
elif name == "Validity Threshold":
socket.default_value, socket.min_value, socket.max_value = 0.9, 0.0, 1.0
socket.subtype = "FACTOR"
elif name == "Material":
socket.hide_in_modifier = True
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
links = group.links
def node(kind, x, y):
result = group.nodes.new(kind)
result.location = (x, y)
return result
def math(operation, a, b, x, y):
result = node("ShaderNodeMath", x, y)
result.operation = operation
for index, value in enumerate((a, b)):
if isinstance(value, (float, int)):
result.inputs[index].default_value = value
else:
links.new(value, result.inputs[index])
return result.outputs[0]
def store(geometry, name, field, domain, x, y):
result = node("GeometryNodeStoreNamedAttribute", x, y)
result.data_type, result.domain = "FLOAT_VECTOR", domain
result.inputs["Name"].default_value = name
links.new(geometry, result.inputs["Geometry"])
links.new(field, result.inputs["Value"])
return result.outputs["Geometry"]
def read(name, x, y):
result = node("GeometryNodeInputNamedAttribute", x, y)
result.data_type = "FLOAT_VECTOR"
result.inputs["Name"].default_value = name
return result.outputs["Attribute"]
inputs = node("NodeGroupInput", 0, 400)
segments = math("POWER", 2, inputs.outputs["Subdivide"], 0, 150)
segments = math("ADD", segments, 1, 180, 150)
sphere = node("GeometryNodeGroup", 400, 400)
sphere.node_tree = target.node_groups[0]
sphere.inputs["Radius"].default_value = 1
links.new(segments, sphere.inputs["Segments"])
position = node("GeometryNodeInputPosition", 400, 100)
geometry = store(sphere.outputs["Mesh"], "panorama_direction", position.outputs[0], "POINT", 620, 400)
xyz = node("ShaderNodeSeparateXYZ", 620, 100)
links.new(read("panorama_direction", 400, -80), xyz.inputs[0])
angle = math("ARCTAN2", xyz.outputs["Y"], xyz.outputs["X"], 800, 100)
longitude = math("DIVIDE", angle, 6.283185307179586, 980, 100)
longitude = math("FRACT", math("ADD", longitude, 1, 1160, 100), 0, 1340, 100)
latitude = math("ARCCOSINE", xyz.outputs["Z"], 0, 800, -100)
latitude = math("SUBTRACT", 1, math("DIVIDE", latitude, 3.141592653589793, 980, -100), 1160, -100)
uv = node("ShaderNodeCombineXYZ", 1540, 100)
links.new(longitude, uv.inputs["X"])
links.new(latitude, uv.inputs["Y"])
geometry = store(geometry, "panorama_sample_uv", uv.outputs[0], "POINT", 1540, 400)
# Each face chooses its own longitude branch; pole corners use the face longitude.
face_position = node("GeometryNodeFieldOnDomain", 800, -400)
face_position.data_type, face_position.domain = "FLOAT_VECTOR", "FACE"
links.new(position.outputs[0], face_position.inputs["Value"])
face_xyz = node("ShaderNodeSeparateXYZ", 980, -400)
links.new(face_position.outputs[0], face_xyz.inputs[0])
face_angle = math("ARCTAN2", face_xyz.outputs["Y"], face_xyz.outputs["X"], 1160, -400)
face_u = math("DIVIDE", face_angle, 6.283185307179586, 1340, -400)
correction = math("ROUND", math("SUBTRACT", face_u, longitude, 1540, -300), 0, 1720, -300)
corner_u = math("ADD", longitude, correction, 1900, -300)
pole = math("GREATER_THAN", math("ABSOLUTE", xyz.outputs["Z"], 0, 1540, -520), 0.999999, 1720, -520)
choose_u = node("GeometryNodeSwitch", 2080, -300)
choose_u.input_type = "FLOAT"
links.new(pole, choose_u.inputs["Switch"])
links.new(corner_u, choose_u.inputs["False"])
links.new(face_u, choose_u.inputs["True"])
corner_uv = node("ShaderNodeCombineXYZ", 2300, -300)
links.new(choose_u.outputs[0], corner_uv.inputs["X"])
links.new(latitude, corner_uv.inputs["Y"])
geometry = store(geometry, "UVMap", corner_uv.outputs[0], "CORNER", 2300, 400)
samples = node("NodeGroupInput", 2520, -150)
sample_uv = read("panorama_sample_uv", 2520, -400)
texture = node("GeometryNodeImageTexture", 2740, 0)
texture.interpolation, texture.extension = "Closest", "EXTEND"
links.new(samples.outputs["Distance"], texture.inputs["Image"])
links.new(sample_uv, texture.inputs["Vector"])
depth = node("ShaderNodeSeparateXYZ", 2960, 0)
links.new(texture.outputs["Color"], depth.inputs[0])
blend = node("ShaderNodeMapRange", 3180, 0)
blend.clamp = True
blend.inputs["To Min"].default_value = 1
links.new(inputs.outputs["Depth Scale"], blend.inputs["Value"])
links.new(depth.outputs["X"], blend.inputs["To Max"])
radial = node("ShaderNodeVectorMath", 3400, 0)
radial.operation = "SCALE"
links.new(read("panorama_direction", 3180, -260), radial.inputs[0])
links.new(blend.outputs["Result"], radial.inputs[3])
deform = node("GeometryNodeSetPosition", 3620, 400)
links.new(geometry, deform.inputs["Geometry"])
links.new(radial.outputs[0], deform.inputs["Position"])
channels = node("FunctionNodeSeparateColor", 2960, -430)
channels.mode = "RGB"
links.new(texture.outputs["Color"], channels.inputs["Color"])
invalid = node("FunctionNodeCompare", 3400, -500)
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
links.new(channels.outputs["Alpha"], invalid.inputs["A"])
links.new(inputs.outputs["Validity Threshold"], invalid.inputs["B"])
point_invalid = node("GeometryNodeFieldOnDomain", 3620, -500)
point_invalid.data_type, point_invalid.domain = "BOOLEAN", "POINT"
links.new(invalid.outputs["Result"], point_invalid.inputs["Value"])
delete = node("GeometryNodeDeleteGeometry", 3840, 400)
delete.domain, delete.mode = "FACE", "ALL"
links.new(deform.outputs["Geometry"], delete.inputs["Geometry"])
links.new(point_invalid.outputs[0], delete.inputs["Selection"])
material = node("GeometryNodeSetMaterial", 4060, 400)
links.new(delete.outputs["Geometry"], material.inputs["Geometry"])
links.new(inputs.outputs["Material"], material.inputs["Material"])
output = node("NodeGroupOutput", 4280, 400)
links.new(material.outputs["Geometry"], output.inputs[0])
prepare_node_group(group)
return group