"""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