- 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.
152 lines
7.5 KiB
Python
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
|