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.
This commit is contained in:
185
experiments/panorama_surface/blender_scene.py
Normal file
185
experiments/panorama_surface/blender_scene.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Build or verify a standalone panorama Geometry Nodes demonstration offline."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from quad_surface import build_node_group
|
||||
from validation import evaluated_points, socket_id, verify
|
||||
|
||||
|
||||
def create_distance_image(name, path, output_path):
|
||||
with np.load(path) as data:
|
||||
distance, alpha = data["distance"], data["alpha"]
|
||||
height, width = distance.shape
|
||||
image = bpy.data.images.new(name, width=width, height=height, alpha=True, float_buffer=True)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
rgba = np.concatenate((np.repeat(distance[..., None], 3, -1), alpha[..., None]), -1)
|
||||
image.pixels.foreach_set(rgba[::-1].astype(np.float32).ravel())
|
||||
image.filepath_raw = str(output_path)
|
||||
image.file_format = "OPEN_EXR"
|
||||
image.save()
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def create_material(image):
|
||||
material = bpy.data.materials.new("Panorama Color")
|
||||
material.use_nodes = True
|
||||
material.node_tree.nodes.clear()
|
||||
nodes, links = material.node_tree.nodes, material.node_tree.links
|
||||
uv = nodes.new("ShaderNodeUVMap")
|
||||
uv.uv_map = "UVMap"
|
||||
uv.location = (0, 0)
|
||||
texture = nodes.new("ShaderNodeTexImage")
|
||||
texture.image = image
|
||||
texture.extension = "REPEAT"
|
||||
texture.location = (200, 0)
|
||||
emission = nodes.new("ShaderNodeEmission")
|
||||
emission.location = (500, 0)
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
output.location = (700, 0)
|
||||
links.new(uv.outputs[0], texture.inputs[0])
|
||||
links.new(texture.outputs[0], emission.inputs[0])
|
||||
links.new(emission.outputs[0], output.inputs[0])
|
||||
return material
|
||||
|
||||
|
||||
def build_scene(output):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = build_node_group()
|
||||
color = bpy.data.images.load(str(output / "panorama.png"))
|
||||
color.pack()
|
||||
material = create_material(color)
|
||||
objects = []
|
||||
for name, filename, offset in (("Reference Room", "reference.npz", -4),
|
||||
("MoGe2 Panorama", "prediction.npz", 4)):
|
||||
if not (output / filename).exists():
|
||||
continue
|
||||
depth_path = output / ("reference_depth.exr" if filename == "reference.npz" else "depth.exr")
|
||||
distance = create_distance_image(depth_path.name, output / filename, depth_path)
|
||||
mesh = bpy.data.meshes.new("Panorama Host")
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.location.x = offset
|
||||
obj.data.materials.append(material)
|
||||
modifier = obj.modifiers.new("Panorama Surface", "NODES")
|
||||
modifier.node_group = group
|
||||
modifier[socket_id(group, "Distance")] = distance
|
||||
modifier[socket_id(group, "Subdivide")] = 7
|
||||
modifier[socket_id(group, "Depth Scale")] = 1.0
|
||||
modifier[socket_id(group, "Validity Threshold")] = 0.9
|
||||
modifier[socket_id(group, "Material")] = material
|
||||
objects.append(obj)
|
||||
for obj in objects:
|
||||
obj.select_set(False)
|
||||
objects[-1].select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[-1]
|
||||
scene = bpy.context.scene
|
||||
scene.render.engine = "CYCLES"
|
||||
scene.cycles.samples = 16
|
||||
scene.render.resolution_x, scene.render.resolution_y = 1400, 750
|
||||
scene.render.resolution_percentage = 100
|
||||
scene.world = bpy.data.worlds.new("Trial World")
|
||||
scene.world.color = (0.18, 0.18, 0.18)
|
||||
scene.view_settings.view_transform = "Standard"
|
||||
camera_data = bpy.data.cameras.new("Overview")
|
||||
camera = bpy.data.objects.new("Overview", camera_data)
|
||||
bpy.context.collection.objects.link(camera)
|
||||
camera.location = (11, -17, 13)
|
||||
camera.rotation_euler = (Vector((0, 0, 0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
|
||||
camera_data.type, camera_data.ortho_scale = "ORTHO", 20
|
||||
if len(objects) == 1:
|
||||
points, _ = evaluated_points(objects[0])
|
||||
points += np.array(objects[0].location)
|
||||
center = (points.min(0) + points.max(0)) / 2
|
||||
extent = float(np.max(np.ptp(points, axis=0)))
|
||||
camera.location = Vector(center) + Vector((0.6, -1, 1)) * extent
|
||||
camera.rotation_euler = (Vector(center) - camera.location).to_track_quat("-Z", "Y").to_euler()
|
||||
camera_data.ortho_scale = extent * 1.8
|
||||
scene.camera = camera
|
||||
capture_camera = None
|
||||
if len(objects) == 1:
|
||||
capture_data = bpy.data.cameras.new("Capture Center")
|
||||
capture_data.lens = 22
|
||||
capture_camera = bpy.data.objects.new("Capture Center", capture_data)
|
||||
bpy.context.collection.objects.link(capture_camera)
|
||||
capture_camera.location = objects[0].location
|
||||
capture_camera.rotation_euler = Vector((-1, 0, 0.15)).to_track_quat("-Z", "Y").to_euler()
|
||||
bpy.data.texts.new("Attribution").write(
|
||||
(Path(__file__).parent / "ATTRIBUTION.md").read_text(encoding="utf-8"))
|
||||
bpy.data.texts.new("试验说明").write(
|
||||
"Subdivide 控制 Quad Sphere 细分,全有效面数为 6 × 4^Subdivide。\n"
|
||||
"Depth Scale:0 为单位球面,1 为全景距离重建。\n"
|
||||
"深度 EXR 的 Alpha 已包含 Mask × 源图 Alpha,低于 Validity Threshold 的区域会被剔除。\n"
|
||||
"Quad Sphere 来自 O_Essentials,节点组和依赖已打包。\n"
|
||||
)
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.spaces.active.shading.type = "MATERIAL"
|
||||
area.spaces.active.region_3d.view_distance = extent * 1.4 if len(objects) == 1 else 18
|
||||
if len(objects) == 1:
|
||||
area.spaces.active.region_3d.view_location = Vector(center)
|
||||
area.spaces.active.region_3d.view_rotation = camera.rotation_euler.to_quaternion()
|
||||
verify(output)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(output / "panorama_trial.blend"))
|
||||
# The render uses a temporary inspection cut; the saved asset retains the full shell.
|
||||
for obj in objects:
|
||||
if len(objects) == 1:
|
||||
continue
|
||||
modifier = obj.modifiers.new("Inspection Cut", "NODES")
|
||||
cut_group = bpy.data.node_groups.new(f"{obj.name} Inspection", "GeometryNodeTree")
|
||||
cut_group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
cut_group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = cut_group.nodes.new("NodeGroupInput")
|
||||
position = cut_group.nodes.new("GeometryNodeInputPosition")
|
||||
xyz = cut_group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
above = cut_group.nodes.new("ShaderNodeMath")
|
||||
above.operation = "GREATER_THAN"
|
||||
above.inputs[1].default_value = 0.4
|
||||
front = cut_group.nodes.new("ShaderNodeMath")
|
||||
front.operation = "LESS_THAN"
|
||||
front.inputs[1].default_value = -0.7
|
||||
either = cut_group.nodes.new("ShaderNodeMath")
|
||||
either.operation = "MAXIMUM"
|
||||
remove = cut_group.nodes.new("GeometryNodeDeleteGeometry")
|
||||
remove.domain = "FACE"
|
||||
result = cut_group.nodes.new("NodeGroupOutput")
|
||||
for a, b in ((position.outputs[0], xyz.inputs[0]),
|
||||
(xyz.outputs["Z"], above.inputs[0]), (xyz.outputs["Y"], front.inputs[0]),
|
||||
(above.outputs[0], either.inputs[0]), (front.outputs[0], either.inputs[1]),
|
||||
(source.outputs[0], remove.inputs["Geometry"]),
|
||||
(either.outputs[0], remove.inputs["Selection"]),
|
||||
(remove.outputs[0], result.inputs[0])):
|
||||
cut_group.links.new(a, b)
|
||||
modifier.node_group = cut_group
|
||||
scene.render.filepath = str(output / "comparison.png")
|
||||
bpy.ops.render.render(write_still=True)
|
||||
if capture_camera is not None:
|
||||
scene.camera = capture_camera
|
||||
scene.render.filepath = str(output / "capture_view.png")
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--verify", action="store_true")
|
||||
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
output = args.output.resolve()
|
||||
if args.verify:
|
||||
bpy.ops.wm.open_mainfile(filepath=str(output / "panorama_trial.blend"))
|
||||
verify(output)
|
||||
else:
|
||||
build_scene(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user