- 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.
186 lines
8.3 KiB
Python
186 lines
8.3 KiB
Python
"""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()
|