From 4309ea9dae6a96563f766609c230ea6c2383bc97 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 6 Jan 2026 14:46:14 +0100 Subject: [PATCH 01/13] first commit --- BlenderExtension/see_blender/__init__.py | 4 +- BlenderExtension/see_blender/exporter.py | 2 + BlenderExtension/see_blender/importer.py | 257 +++++++++ BlenderExtension/see_blender_link/__init__.py | 20 + .../addons/cursor_tracker/__init__.py | 200 +++++++ .../addons/path_viewer/__init__.py | 118 +++++ .../addons/path_viewer/commands/__init__.py | 0 .../path_viewer/commands/click_on_node.py | 24 + .../path_viewer/commands/create_path.py | 158 ++++++ .../path_viewer/commands/dbclick_on_node.py | 73 +++ .../path_viewer/commands/delete_path.py | 24 + .../path_viewer/commands/import_scene.py | 27 + .../path_viewer/commands/select_path.py | 19 + .../addons/path_viewer/dispatcher.py | 15 + BlenderExtension/see_blender_link/config.py | 4 + .../see_blender_link/core/__init__.py | 0 .../see_blender_link/core/dispatcher.py | 26 + .../see_blender_link/core/receiver.py | 14 + .../see_blender_link/transport/__init__.py | 0 .../see_blender_link/transport/receiver.py | 75 +++ .../see_blender_link/transport/sender.py | 30 ++ .../see_blender_link/utils/__init__.py | 0 .../see_blender_link/utils/helper.py | 28 + SeeSharp.Blazor/GraphBrowser.razor | 92 ++++ SeeSharp.Blazor/GraphBrowser.razor.css | 26 + SeeSharp.Blender/Addons/CursorTracker.cs | 13 + SeeSharp.Blender/Addons/PathViewer.cs | 22 + SeeSharp.Blender/BlenderCommandSender.cs | 129 +++++ SeeSharp.Blender/BlenderEventDispatcher.cs | 46 ++ SeeSharp.Blender/BlenderEventListener.cs | 66 +++ SeeSharp.Blender/Example/App.razor | 12 + .../Data/CornellBox/Meshes/Cube.0.0.ply | Bin 0 -> 1184 bytes .../Data/CornellBox/Meshes/Plane.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Plane.001.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Plane.002.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Plane.003.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Plane.004.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Plane.005.0.0.ply | Bin 0 -> 458 bytes .../Data/CornellBox/Meshes/Sphere.0.0.ply | Bin 0 -> 72254 bytes .../Example/Data/CornellBox/cornellbox.json | 490 ++++++++++++++++++ .../Data/ExportTest/Meshes/Cube.0.0.ply | Bin 0 -> 1184 bytes .../Data/ExportTest/Meshes/Plane.0.0.ply | Bin 0 -> 458 bytes .../Data/ExportTest/Meshes/Sphere.0.0.ply | Bin 0 -> 72254 bytes .../Example/Data/ExportTest/exporttest.json | 275 ++++++++++ SeeSharp.Blender/Example/Imports.cs | 32 ++ SeeSharp.Blender/Example/MainLayout.razor | 3 + .../Example/Pages/BlenderImporter.razor | 119 +++++ .../Example/Pages/BlenderImporter.razor.css | 66 +++ .../Example/Pages/CursorTracker.razor | 44 ++ .../Example/Pages/Experiment.razor | 97 ++++ .../Example/Pages/Experiment.razor.cs | 66 +++ SeeSharp.Blender/Example/Pages/Index.razor | 39 ++ .../Example/Pages/PathViewer.razor | 232 +++++++++ .../Example/Pages/PathViewer.razor.cs | 214 ++++++++ SeeSharp.Blender/Example/Pages/_Host.cshtml | 35 ++ SeeSharp.Blender/Example/Program.cs | 45 ++ .../Example/SeeSharp.Blender.Example.csproj | 18 + SeeSharp.Blender/Example/_Imports.razor | 6 + .../Example/appsettings.Development.json | 9 + SeeSharp.Blender/Example/appsettings.json | 9 + SeeSharp.Blender/Example/wwwroot/css/site.css | 86 +++ SeeSharp.Blender/Example/wwwroot/raycast.js | 23 + SeeSharp.Blender/Handlers/CreatedHandler.cs | 13 + .../Handlers/CursorTrackedHandler.cs | 38 ++ SeeSharp.Blender/Handlers/DeletedHandler.cs | 12 + SeeSharp.Blender/Handlers/SelectedHandler.cs | 12 + SeeSharp.Blender/SeeSharp.Blender.csproj | 12 + 67 files changed, 3518 insertions(+), 1 deletion(-) create mode 100644 BlenderExtension/see_blender/importer.py create mode 100644 BlenderExtension/see_blender_link/__init__.py create mode 100644 BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/__init__.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/__init__.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py create mode 100644 BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py create mode 100644 BlenderExtension/see_blender_link/config.py create mode 100644 BlenderExtension/see_blender_link/core/__init__.py create mode 100644 BlenderExtension/see_blender_link/core/dispatcher.py create mode 100644 BlenderExtension/see_blender_link/core/receiver.py create mode 100644 BlenderExtension/see_blender_link/transport/__init__.py create mode 100644 BlenderExtension/see_blender_link/transport/receiver.py create mode 100644 BlenderExtension/see_blender_link/transport/sender.py create mode 100644 BlenderExtension/see_blender_link/utils/__init__.py create mode 100644 BlenderExtension/see_blender_link/utils/helper.py create mode 100644 SeeSharp.Blazor/GraphBrowser.razor create mode 100644 SeeSharp.Blazor/GraphBrowser.razor.css create mode 100644 SeeSharp.Blender/Addons/CursorTracker.cs create mode 100644 SeeSharp.Blender/Addons/PathViewer.cs create mode 100644 SeeSharp.Blender/BlenderCommandSender.cs create mode 100644 SeeSharp.Blender/BlenderEventDispatcher.cs create mode 100644 SeeSharp.Blender/BlenderEventListener.cs create mode 100644 SeeSharp.Blender/Example/App.razor create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.001.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Sphere.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/CornellBox/cornellbox.json create mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Cube.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Plane.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Sphere.0.0.ply create mode 100644 SeeSharp.Blender/Example/Data/ExportTest/exporttest.json create mode 100644 SeeSharp.Blender/Example/Imports.cs create mode 100644 SeeSharp.Blender/Example/MainLayout.razor create mode 100644 SeeSharp.Blender/Example/Pages/BlenderImporter.razor create mode 100644 SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css create mode 100644 SeeSharp.Blender/Example/Pages/CursorTracker.razor create mode 100644 SeeSharp.Blender/Example/Pages/Experiment.razor create mode 100644 SeeSharp.Blender/Example/Pages/Experiment.razor.cs create mode 100644 SeeSharp.Blender/Example/Pages/Index.razor create mode 100644 SeeSharp.Blender/Example/Pages/PathViewer.razor create mode 100644 SeeSharp.Blender/Example/Pages/PathViewer.razor.cs create mode 100644 SeeSharp.Blender/Example/Pages/_Host.cshtml create mode 100644 SeeSharp.Blender/Example/Program.cs create mode 100644 SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj create mode 100644 SeeSharp.Blender/Example/_Imports.razor create mode 100644 SeeSharp.Blender/Example/appsettings.Development.json create mode 100644 SeeSharp.Blender/Example/appsettings.json create mode 100644 SeeSharp.Blender/Example/wwwroot/css/site.css create mode 100644 SeeSharp.Blender/Example/wwwroot/raycast.js create mode 100644 SeeSharp.Blender/Handlers/CreatedHandler.cs create mode 100644 SeeSharp.Blender/Handlers/CursorTrackedHandler.cs create mode 100644 SeeSharp.Blender/Handlers/DeletedHandler.cs create mode 100644 SeeSharp.Blender/Handlers/SelectedHandler.cs create mode 100644 SeeSharp.Blender/SeeSharp.Blender.csproj diff --git a/BlenderExtension/see_blender/__init__.py b/BlenderExtension/see_blender/__init__.py index 5758292a..47c46164 100644 --- a/BlenderExtension/see_blender/__init__.py +++ b/BlenderExtension/see_blender/__init__.py @@ -1,4 +1,4 @@ -from . import exporter, render_engine, material_ui, material, world +from . import exporter, render_engine, material_ui, material, world, importer def register(): exporter.register() @@ -6,6 +6,7 @@ def register(): material_ui.register() material.register() world.register() + importer.register() def unregister(): exporter.unregister() @@ -13,3 +14,4 @@ def unregister(): material_ui.unregister() material.unregister() world.unregister() + importer.unregister() diff --git a/BlenderExtension/see_blender/exporter.py b/BlenderExtension/see_blender/exporter.py index 00975799..d862f79b 100644 --- a/BlenderExtension/see_blender/exporter.py +++ b/BlenderExtension/see_blender/exporter.py @@ -140,6 +140,8 @@ def material_to_json(material, out_dir): material.emission_color[1] * material.emission_strength, material.emission_color[2] * material.emission_strength )), + "emission_color": map_rgb(material.emission_color), + "emission_strength": material.emission_strength, "emissionIsGlossy": material.emission_is_glossy, "emissionExponent": material.emission_glossy_exponent } diff --git a/BlenderExtension/see_blender/importer.py b/BlenderExtension/see_blender/importer.py new file mode 100644 index 00000000..4f85c99f --- /dev/null +++ b/BlenderExtension/see_blender/importer.py @@ -0,0 +1,257 @@ +import os +import json +import math +import bpy +import mathutils +from bpy_extras.io_utils import axis_conversion + +# ------------------------------------------------------------------------ +# Utility +# ------------------------------------------------------------------------ + +def load_image(path): + """Loads image into Blender or returns existing.""" + abspath = bpy.path.abspath(path) + if not os.path.exists(abspath): + print(f"WARNING: Missing texture: {abspath}") + return None + img = bpy.data.images.load(abspath, check_existing=True) + return img + + +def make_material(name, mat_json, base_path): + """Create a Blender material based on SeeSharp material JSON definition.""" + mat = bpy.data.materials.new(name) + mat.use_nodes = True + nt = mat.node_tree + nodes = nt.nodes + links = nt.links + + nodes.clear() + + output = nodes.new("ShaderNodeOutputMaterial") + principled = nodes.new("ShaderNodeBsdfPrincipled") + principled.location = (-200, 0) + output.location = (200, 0) + links.new(principled.outputs["BSDF"], output.inputs["Surface"]) + + # -------- Base color (texture or rgb) + base_color = mat_json["baseColor"] + if base_color["type"] == "rgb": + principled.inputs["Base Color"].default_value = base_color["value"] + [1.0] + elif base_color["type"] == "image": + img_path = os.path.join(base_path, base_color["filename"]) + img = load_image(img_path) + if img: + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + links.new(tex.outputs["Color"], principled.inputs["Base Color"]) + + # -------- Roughness (texture or float) + roughness = mat_json["roughness"] + if isinstance(roughness, str): # texture + img_path = os.path.join(base_path, roughness) + img = load_image(img_path) + if img: + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + tex.location = (-200, -250) + links.new(tex.outputs["Color"], principled.inputs["Roughness"]) + else: + principled.inputs["Roughness"].default_value = float(roughness) + + # Metallic, IOR, Anisotropic + principled.inputs["Metallic"].default_value = mat_json.get("metallic", 0.0) + principled.inputs["IOR"].default_value = mat_json.get("IOR", 1.45) + principled.inputs["Anisotropic"].default_value = mat_json.get("anisotropic", 0.0) + + # Emission + emission_json = mat_json["emission"] + if emission_json["type"] == "rgb": + color = mat_json["emission_color"]["value"] + principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) + principled.inputs["Emission Strength"].default_value = mat_json["emission_strength"] + if mat_json.get("emissionIsGlossy", False): + principled.inputs["Emission Strength"].default_value = mat_json["emissionExponent"] + + return mat + + +def load_ply(filepath): + """Load a .ply mesh and return the created object.""" + before = set(bpy.data.objects) + bpy.ops.wm.ply_import(filepath=filepath) + after = set(bpy.data.objects) + + new_objs = list(after - before) + if new_objs: + return new_objs[0] + return None + + +# ------------------------------------------------------------------------ +# Camera +# ------------------------------------------------------------------------ + +def import_camera(cam_json, transform_json, scene): + """Recreate SeeSharp camera""" + cam_data = bpy.data.cameras.new("Camera") + cam_obj = bpy.data.objects.new("Camera", cam_data) + scene.collection.objects.link(cam_obj) + scene.camera = cam_obj + + # ------------ Transform + pos = transform_json["position"] + rot = transform_json["rotation"] + + cam_obj.location = (-pos[0], pos[2], pos[1]) + + # inverse Euler mapping + eul = mathutils.Euler(( + math.radians(rot[0] + 90), # x_euler + math.radians(rot[2]), # y_euler + math.radians(rot[1] - 180) # z_euler + ), 'XYZ') + cam_obj.rotation_euler = eul + + # ------------ FOV (vertical → horizontal) + vert_fov = math.radians(cam_json["fov"]) + aspect = scene.render.resolution_y / scene.render.resolution_x + horiz_fov = 2 * math.atan(math.tan(vert_fov / 2) / aspect) + cam_data.angle = horiz_fov + + return cam_obj + + +# ------------------------------------------------------------------------ +# Background HDR +# ------------------------------------------------------------------------ + +def import_background(bg_json, base_path): + world = bpy.context.scene.world + world.use_nodes = True + nt = world.node_tree + + env_tex = nt.nodes.new("ShaderNodeTexEnvironment") + env_tex.location = (-300, 0) + + fname = os.path.join(base_path, bg_json["filename"]) + img = load_image(fname) + if img: + env_tex.image = img + + bg = nt.nodes["Background"] + nt.links.new(env_tex.outputs["Color"], bg.inputs["Color"]) + + +# ------------------------------------------------------------------------ +# Main Import Logic +# ------------------------------------------------------------------------ + +def import_seesharp(filepath): + with open(filepath, "r") as f: + data = json.load(f) + + base_path = os.path.dirname(filepath) + + scene = bpy.context.scene + + # ------------------------------------------------------------------ + # Materials + # ------------------------------------------------------------------ + mat_lookup = {} + if "materials" in data: + for m in data["materials"]: + mat = make_material(m["name"], m, base_path) + mat_lookup[m["name"]] = mat + + # ------------------------------------------------------------------ + # Background + # ------------------------------------------------------------------ + if "background" in data: + import_background(data["background"], base_path) + + # ------------------------------------------------------------------ + # Camera + # ------------------------------------------------------------------ + if "cameras" in data and "transforms" in data: + cam_desc = data["cameras"][0] + transform = next(t for t in data["transforms"] if t["name"] == cam_desc["transform"]) + import_camera(cam_desc, transform, scene) + + # ------------------------------------------------------------------ + # Meshes / Objects + # ------------------------------------------------------------------ + global_matrix = axis_conversion(from_forward="Z", from_up="Y").to_4x4() + + if "objects" in data: + for obj in data["objects"]: + ply_path = os.path.join(base_path, obj["relativePath"]) + new_obj = load_ply(ply_path) + if not new_obj: + print(f"Failed to load {ply_path}") + continue + + # Apply SeeSharp → Blender inverse transform + new_obj.matrix_world = global_matrix.inverted() + + # Assign material + if obj["material"] in mat_lookup: + new_obj.data.materials.append(mat_lookup[obj["material"]]) + + bpy.context.scene.render.engine = "SEE_SHARP" + + try: + bpy.ops.seesharp.convert_all_materials() + print("✔ Converted all materials to SeeSharp") + except Exception as e: + print("❌ Failed to convert materials:", e) + + print("SeeSharp scene import finished.") + + +# ------------------------------------------------------------------------ +# Blender Operator +# ------------------------------------------------------------------------ +class SeeSharpImporter(bpy.types.Operator): + """Import SeeSharp scene (.json)""" + bl_idname = "import_scene.seesharp" + bl_label = "Import SeeSharp Scene" + + filename_ext = ".json" + filter_glob: bpy.props.StringProperty( + default="*.json", + options={'HIDDEN'} + ) + + filepath: bpy.props.StringProperty( + name="File Path", + description="Path to SeeSharp JSON scene", + maxlen=1024, + subtype='FILE_PATH' + ) + + def execute(self, context): + import_seesharp(self.filepath) + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + +def menu_func_import(self, context): + self.layout.operator(SeeSharpImporter.bl_idname, text="SeeSharp Scene (.json)") + + +def register(): + bpy.utils.register_class(SeeSharpImporter) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + + +def unregister(): + bpy.utils.unregister_class(SeeSharpImporter) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/__init__.py b/BlenderExtension/see_blender_link/__init__.py new file mode 100644 index 00000000..fb973e93 --- /dev/null +++ b/BlenderExtension/see_blender_link/__init__.py @@ -0,0 +1,20 @@ +bl_info = { + "name": "SeeBlender Link", + "author": "Minh Nguyen", + "version": (1, 0), + "blender": (4, 5, 0), + "description": "Enable communication between Blender and SeeSharp application", + "category": "System", +} + +from .addons import path_viewer, cursor_tracker + +modules = (path_viewer, cursor_tracker,) + +def register(): + for m in modules: + m.register() + +def unregister(): + for m in reversed(modules): + m.unregister() \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py b/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py new file mode 100644 index 00000000..a8137bee --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py @@ -0,0 +1,200 @@ +import bpy +import threading +import socket +import time +import json +from mathutils import Vector +from ...config import HOST, PORT_IN, PORT_OUT +from ...transport.sender import send_to_blazor +stop_flag = False +send_thread = None +last_pos = (None, None, None) +# Continuously updated viewport data +current_area = None +current_region = None +current_rv3d = None + +view_handler = None + + +# -------------------------------------------------------------------- +# VIEW TRACKER (runs inside Blender's UI thread on every redraw) +# -------------------------------------------------------------------- + +def update_view_data(): + """ + This function runs every viewport redraw and always updates + the current view matrix, region, and area. + """ + global current_area, current_region, current_rv3d + + # Look through all windows and areas until we find a VIEW_3D + for window in bpy.context.window_manager.windows: + screen = window.screen + if not screen: + continue + + for area in screen.areas: + if area.type == 'VIEW_3D': + region = next((r for r in area.regions if r.type == 'WINDOW'), None) + if not region: + continue + + rv3d = area.spaces.active.region_3d + if rv3d: + current_area = area + current_region = region + current_rv3d = rv3d + return + + +# -------------------------------------------------------------------- +# TCP sending loop (runs in background thread) +# -------------------------------------------------------------------- + +def raycast_and_send_loop(): + global stop_flag, last_pos + + while not stop_flag: + + # Ensure viewport data is available + if not current_rv3d: + time.sleep(0.05) + continue + + try: + scene = bpy.context.scene + cursor_pos = scene.cursor.location.copy() + + # Get ray origin = user view position + view_matrix = current_rv3d.view_matrix + origin = view_matrix.inverted().translation + + direction = (cursor_pos - origin).normalized() + + depsgraph = bpy.context.evaluated_depsgraph_get() + hit, loc, normal, face_idx, obj, _ = scene.ray_cast( + depsgraph, origin, direction + ) + if loc != last_pos: + last_pos = loc + if hit: + data = { + "event": "cursor_tracked", + "object": obj.name, + "hit_position": [round(loc.x, 4), round(loc.y, 4), round(loc.z, 4)], + "normal": [round(normal.x, 4), round(normal.y, 4), round(normal.z, 4)], + "face_index": face_idx, + "cursor_position": [round(cursor_pos.x, 4), round(cursor_pos.y, 4), round(cursor_pos.z, 4)] + } + else: + data = { + "event": "cursor_tracked", + "object": None, + "cursor_position": [round(cursor_pos.x, 4), round(cursor_pos.y, 4), round(cursor_pos.z, 4)] + } + # s.sendall((json.dumps(data) + "\n").encode("utf8")) + send_to_blazor(data) + # print(data) + print("Sending JSON:", json.dumps(data)) + except Exception as e: + print("Error in thread loop:", e) + break + + time.sleep(0.25) + + # s.close() + print("Stopped sending") + + +# -------------------------------------------------------------------- +# Start / Stop Functions +# -------------------------------------------------------------------- + +def start_sender(): + global stop_flag, send_thread + stop_flag = False + + send_thread = threading.Thread(target=raycast_and_send_loop, daemon=True) + send_thread.start() + print("Sender started") + + +def stop_sender(): + global stop_flag + stop_flag = True + print("Sender stopping...") + + +# -------------------------------------------------------------------- +# UI Panel + Property +# -------------------------------------------------------------------- + +def toggle_sender(self, context): + if self.sending_enabled: + start_sender() + else: + stop_sender() + + +class CursorTrackerProperties(bpy.types.PropertyGroup): + sending_enabled: bpy.props.BoolProperty( + name="Send Cursor Info", + description="Enable real-time raycast + cursor output", + default=False, + update=toggle_sender + ) + + +class CursorTrackerPanel(bpy.types.Panel): + bl_label = "Cursor Tracker" + bl_idname = "cursor_tracker_panel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'SeeSharp' + + def draw(self, context): + layout = self.layout + props = context.scene.cursor_sender_props + layout.prop(props, "sending_enabled") + + +# -------------------------------------------------------------------- +# Registration + View Handler +# -------------------------------------------------------------------- + +classes = ( + CursorTrackerProperties, + CursorTrackerPanel, +) + +def register(): + global view_handler + + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.cursor_sender_props = bpy.props.PointerProperty(type=CursorTrackerProperties) + + # Add draw handler to track view continuously + view_handler = bpy.types.SpaceView3D.draw_handler_add( + update_view_data, (), 'WINDOW', 'POST_VIEW' + ) + print("Add-on registered and view tracking started.") + + +def unregister(): + global view_handler + + stop_sender() + + if view_handler: + bpy.types.SpaceView3D.draw_handler_remove(view_handler, 'WINDOW') + view_handler = None + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.cursor_sender_props + + print("Add-on unregistered.") diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py b/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py new file mode 100644 index 00000000..515ecad9 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py @@ -0,0 +1,118 @@ +import bpy +from ...transport.receiver import start, stop +from ...core.receiver import Receiver +from .dispatcher import dispatcher +import json + +class EdgeProps(bpy.types.PropertyGroup): + json_data: bpy.props.StringProperty(name="Edge JSON Data") + + +class PathViewerProps(bpy.types.PropertyGroup): + enabled: bpy.props.BoolProperty( + name="Enable Path Viewer", + description="Listen for path viewer command sent from Blazor", + default=False, + update=lambda self, context: toggle_receiver(self, context) + ) + +receiver = Receiver(dispatcher) + +def toggle_receiver(self, context): + if self.enabled: + print(receiver.dispatcher._handlers) + start(receiver) + else: + stop() + +class PathViewerPanel(bpy.types.Panel): + bl_label = "Path Viewer" + bl_idname = "VIEW3D_PT_path_viewer" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "SeeSharp" + + def draw(self, context): + layout = self.layout + props = context.scene.path_viewer_props + layout.prop(props, "enabled") + +def draw_dict(layout, data, level=0): + """Recursively draw any dict or list in Blender UI.""" + if isinstance(data, dict): + for key, value in data.items(): + row = layout.row() + if isinstance(value, (dict, list)): + col = layout.column() + col.label(text=f"{key}:") + box = col.box() + draw_dict(box, value, level + 1) + else: + row.label(text=f"{key}: {value}") + elif isinstance(data, list): + for i, entry in enumerate(data): + col = layout.column() + col.label(text=f"[{i}]") + if isinstance(entry, (dict, list)): + box = col.box() + draw_dict(box, entry, level + 1) + else: + col.label(text=str(entry)) + +class EdgePanel(bpy.types.Panel): + bl_label = "Edge Settings" + bl_idname = "EDGE_DATA_PT_panel" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" # IMPORTANT → this puts the panel under Object Data Properties + + @classmethod + def poll(cls, context): + obj = context.object + return obj and obj.get("is_edge") == True + + def draw(self, context): + layout = self.layout + obj = context.object + + json_str = obj.edge_data.json_data + if not json_str: + layout.label(text="No data.") + return + + try: + data = json.loads(json_str) + draw_dict(layout.box(), data) + except Exception as e: + layout.label(text=f"JSON Error: {e}") + +classes = ( + PathViewerProps, + PathViewerPanel, + EdgeProps, + EdgePanel +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.path_viewer_props = bpy.props.PointerProperty( + type=PathViewerProps + ) + + bpy.types.Object.edge_data = bpy.props.PointerProperty(type=EdgeProps) + + print("[BlazorReceiver] Add-on registered") + + +def unregister(): + stop() + del bpy.types.Object.edge_data + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.path_viewer_props + + print("[BlazorReceiver] Add-on unregistered") \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/__init__.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py new file mode 100644 index 00000000..97acdbf7 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py @@ -0,0 +1,24 @@ +import bpy +import json +from ....transport.sender import send_to_blazor + +def handle_click_on_node(msg): + json_string = msg.get("path") + col_id = msg.get("path_id") + node_list = json.loads(json_string) + is_full_graph = msg.get("is_full_graph") + def run(): + collection = bpy.data.collections[f"arrow_group_{col_id}"] + for obj in collection.objects: + obj.hide_set(True) + if (is_full_graph): + for obj in collection.objects: + obj.hide_set(False) + else: + for i in range(len(node_list) - 1): + var1 = node_list[i] + var2 = node_list[i + 1] + for obj in collection.objects: + if obj.name == f"{var1}-{var2}" or obj.name == f"{var2}-{var1}": + obj.hide_set(False) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py new file mode 100644 index 00000000..a4f8902f --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py @@ -0,0 +1,158 @@ +import bpy +from mathutils import Vector +from ....utils.helper import get_scene_scale, renderer_to_blender_world +import json +from ....transport.sender import send_to_blazor +def handle_create_path(msg): + json_string = msg.get("graph") + user_group_id = msg.get("id") + data = json.loads(json_string) + def create_fixed_arrow(A, B, properties, name="Arrow"): + A = Vector(A) + B = Vector(B) + dir_vec = (B - A) + total_len = dir_vec.length + if total_len < 1e-6: + return None + + dir_n = dir_vec.normalized() + + # ---- fixed-size thickness, but *length = A→B exactly* ---- + scene_scale = get_scene_scale() + + tip_len = scene_scale * 0.03 # fixed tip length (~3% scene) + shaft_rad = scene_scale * 0.0025 # fixed thickness (~0.25%) + tip_rad = scene_scale * 0.008 # tip radius (~0.8%) + + # clamp tip length if segment is short + tip_len = min(tip_len, total_len * 0.4) + + shaft_len = max(total_len - tip_len, total_len * 0.05) + + # ---- place shaft and tip ---- + shaft_loc = A + dir_n * (shaft_len * 0.5) + tip_loc = A + dir_n * (shaft_len + tip_len * 0.5) + + # cleanup + bpy.ops.object.select_all(action='DESELECT') + + bpy.ops.mesh.primitive_cylinder_add( + radius=shaft_rad, depth=shaft_len, location=shaft_loc) + shaft = bpy.context.active_object + + bpy.ops.mesh.primitive_cone_add( + radius1=tip_rad, depth=tip_len, location=tip_loc) + tip = bpy.context.active_object + + # rotate to direction + up = Vector((0,0,1)) + rot_q = up.rotation_difference(dir_n) + for obj in (shaft, tip): + obj.rotation_mode = 'QUATERNION' + obj.rotation_quaternion = rot_q + + # join + shaft.select_set(True) + bpy.context.view_layer.objects.active = shaft + tip.select_set(True) + bpy.ops.object.join() + obj = bpy.context.active_object + obj.name = name + bpy.ops.object.shade_smooth() + + obj.edge_data.json_data = json.dumps(properties, indent=2) + obj["is_edge"] = True + return obj + + def assign_colour(obj, typeA, typeB, base_name): + rgb = (1.0, 0.0, 0.0) + if (typeA == "BSDFSampleNode"): + if (typeB == "BSDFSampleNode"): + rgb = (1.0, 0.0, 0.0) + elif (typeB == "NextEventNode"): + rgb = (0.0, 0.0, 1.0) + elif (typeB == "BackgroundNode"): + rgb = (0.5, 0.0, 0.5) + else: + rgb = (1.0, 0.0, 0.0) + if (typeA == "LightPathNode" or typeB == "LightPathNode"): + rgb = (0.0, 1.0, 0.0) + + # Create material with color + mat = bpy.data.materials.new(name=base_name + "_mat") + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + if bsdf: + bsdf.inputs["Base Color"].default_value = (rgb[0], rgb[1], rgb[2], 1) + obj.data.materials.append(mat) + return obj + + def edge_type(start, end): + if (end == "BSDFSampleNode"): + return "BSDF" + elif (end == "NextEventNode"): + return "Next Event" + elif (end == "BackgroundNode"): + return "Background" + elif (end == "LightPathNode"): + return "Light Path" + elif (end == "ConnectionNode"): + if (start == "BSDFSampleNode"): + return "Camera Path - Connection" + elif (start == "LightPathNode"): + return "Light Path - Connection" + elif (end == "MergeNode"): + if (start == "BSDFSampleNode"): + return "Camera Path - Merge" + elif (start == "LightPathNode"): + return "Light Path - Merge" + else: + return "Invalid" + + def run(): + col_name = f"arrow_group_{user_group_id}" + col = bpy.data.collections.new(col_name) + bpy.context.scene.collection.children.link(col) + + id_to_node = {} + for node in data["nodes"]: + pos = node["position"] + id_to_node[node["id"]] = {"pos": renderer_to_blender_world(Vector((pos["X"], pos["Y"], pos["Z"]))), + "data": node["data"], + "type": node["type"]} + + for start_id, end_id in data["edges"]: + if (id_to_node[start_id]["type"] == "LightPathNode" or id_to_node[end_id]["type"] == "LightPathNode"): + # reverse the direction if its light path + temp_id = start_id + start_id = end_id + end_id = temp_id + start = id_to_node[start_id]["pos"] #id_to_node.get(start_id) + end = id_to_node[end_id]["pos"] #id_to_node.get(end_id) + #----------------------------------------Add type------------------------------- + arrow_type = edge_type(id_to_node[start_id]["type"], id_to_node[end_id]["type"]) + props = id_to_node[end_id]["data"] + props = dict([("Type", arrow_type), *props.items()]) + #------------------------------------------------------------------------------- + if start is None or end is None: + print(f"Missing node for edge: {start_id} → {end_id}") + continue + + arrow_name = f"Arrow_{start_id}_to_{end_id}" + obj = create_fixed_arrow(start, end, props, f"{start_id}-{end_id}") + obj = assign_colour(obj, id_to_node[start_id]["type"], id_to_node[end_id]["type"], f"{start_id}-{end_id}") + if obj: + col.objects.link(obj) + # Remove from master scene collection (avoid duplicate visible link) + try: + bpy.context.scene.collection.objects.unlink(obj) + except: + pass + + # Tag object with group ID + obj["blazor_arrow_group"] = user_group_id + send_to_blazor({ + "event": "created", + "id": user_group_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py new file mode 100644 index 00000000..4a176ecf --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py @@ -0,0 +1,73 @@ +import bpy +import json +from ....transport.sender import send_to_blazor + +def handle_dbclick_on_node(msg): + json_string = msg.get("path") + col_id = msg.get("path_id") + node_list = json.loads(json_string) + is_full_graph = msg.get("is_full_graph") + + def frame_object(obj): + # Ensure Object Mode + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Deselect all + bpy.ops.object.select_all(action='DESELECT') + + # Select and activate object + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Find a VIEW_3D area + for area in bpy.context.window.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + ): + bpy.ops.view3d.view_selected() + return + + print("No VIEW_3D area found") + + def view_all(center=False): + # Ensure Object Mode (safe for view operators) + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Find a VIEW_3D area + for area in bpy.context.window.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + ): + bpy.ops.view3d.view_all(center=center) + return + + print("No VIEW_3D area found") + def run(): + collection = bpy.data.collections[f"arrow_group_{col_id}"] + for obj in collection.objects: + obj.hide_set(True) + if (is_full_graph): + for obj in collection.objects: + obj.hide_set(False) + view_all(center=False) + else: + for i in range(len(node_list) - 1): + var1 = node_list[i] + var2 = node_list[i + 1] + for obj in collection.objects: + if obj.name == f"{var1}-{var2}" or obj.name == f"{var2}-{var1}": + obj.hide_set(False) + frame_object(obj) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py new file mode 100644 index 00000000..ffc73047 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py @@ -0,0 +1,24 @@ +import bpy +from ....transport.sender import send_to_blazor +def handle_delete_path(msg): + group_id = msg.get("id") + def run(): + col_name = f"arrow_group_{group_id}" + col = bpy.data.collections.get(col_name) + if not col: + print(f"No arrow group found: {col_name}") + return + + # Remove all objects inside + for obj in list(col.objects): + bpy.data.objects.remove(obj, do_unlink=True) + + # Remove the collection itself + bpy.data.collections.remove(col) + + print(f"[ArrowGroup] Deleted group {group_id}") + send_to_blazor({ + "event": "deleted", + "id": group_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py new file mode 100644 index 00000000..d3f7dea0 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py @@ -0,0 +1,27 @@ +import bpy + +def handle_import_scene(msg): + file_name = msg.get("scene_name") + def run(): + # clear the current scene + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + + # Remove all meshes, materials, images, etc. + for block in bpy.data.meshes: + bpy.data.meshes.remove(block) + for block in bpy.data.materials: + bpy.data.materials.remove(block) + for block in bpy.data.textures: + bpy.data.textures.remove(block) + for block in bpy.data.images: + bpy.data.images.remove(block) + + scene_collection = bpy.context.scene.collection + for col in list(bpy.data.collections): + if col != scene_collection: + bpy.data.collections.remove(col) + + bpy.ops.import_scene.seesharp(filepath=file_name) + + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py new file mode 100644 index 00000000..cbdc66b3 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py @@ -0,0 +1,19 @@ +import bpy +from ....transport.sender import send_to_blazor +def handle_select_path(msg): + obj_id = msg.get("id") + col_id = f"arrow_group_{obj_id}" + def run(): + + col = bpy.data.collections.get(col_id) + bpy.ops.object.select_all(action='DESELECT') + # Select all objects in the collection + for obj in col.objects: + obj.select_set(True) + bpy.context.view_layer.objects.active = col.objects[0] if len(col.objects) else None + + send_to_blazor({ + "event": "selected", + "id": obj_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py b/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py new file mode 100644 index 00000000..dfcaed35 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py @@ -0,0 +1,15 @@ +from ...core.dispatcher import Dispatcher +from .commands.create_path import handle_create_path +from .commands.delete_path import handle_delete_path +from .commands.select_path import handle_select_path +from .commands.click_on_node import handle_click_on_node +from .commands.dbclick_on_node import handle_dbclick_on_node +from .commands.import_scene import handle_import_scene + +dispatcher = Dispatcher() +dispatcher.register("create_path", handle_create_path) +dispatcher.register("delete_path", handle_delete_path) +dispatcher.register("select_path", handle_select_path) +dispatcher.register("click_on_node", handle_click_on_node) +dispatcher.register("dbclick_on_node", handle_dbclick_on_node) +dispatcher.register("import_scene", handle_import_scene) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/config.py b/BlenderExtension/see_blender_link/config.py new file mode 100644 index 00000000..885e2919 --- /dev/null +++ b/BlenderExtension/see_blender_link/config.py @@ -0,0 +1,4 @@ +# config.py +HOST = "127.0.0.1" +PORT_IN = 5051 +PORT_OUT = 5052 \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/core/__init__.py b/BlenderExtension/see_blender_link/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/core/dispatcher.py b/BlenderExtension/see_blender_link/core/dispatcher.py new file mode 100644 index 00000000..fe885380 --- /dev/null +++ b/BlenderExtension/see_blender_link/core/dispatcher.py @@ -0,0 +1,26 @@ +class Dispatcher: + def __init__(self): + self._handlers = {} + + def register(self, command: str, handler): + """ + handler: function(msg: dict) + """ + self._handlers.setdefault(command, []).append(handler) + + def dispatch(self, msg: dict): + cmd = msg.get("command") + if not cmd: + print("[Dispatcher] Missing command") + return + + handlers = self._handlers.get(cmd) + if not handlers: + print(f"[Dispatcher] No handlers for '{cmd}'") + return + + for handler in handlers: + try: + handler(msg) + except Exception as e: + print(f"[Dispatcher] Error in handler '{cmd}':", e) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/core/receiver.py b/BlenderExtension/see_blender_link/core/receiver.py new file mode 100644 index 00000000..896962b3 --- /dev/null +++ b/BlenderExtension/see_blender_link/core/receiver.py @@ -0,0 +1,14 @@ +import json + +class Receiver: + def __init__(self, dispatcher): + self.dispatcher = dispatcher + + def handle_message(self, line: str): + if not line.strip(): + return + try: + msg = json.loads(line) + self.dispatcher.dispatch(msg) + except Exception as e: + print("[Receiver] JSON error:", e) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/transport/__init__.py b/BlenderExtension/see_blender_link/transport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/transport/receiver.py b/BlenderExtension/see_blender_link/transport/receiver.py new file mode 100644 index 00000000..7dc143ec --- /dev/null +++ b/BlenderExtension/see_blender_link/transport/receiver.py @@ -0,0 +1,75 @@ +import socket +import threading +import select +from ..config import HOST, PORT_IN +from ..core.receiver import Receiver + +_receiver_thread = None +_stop = False +_server_socket = None + +def _receiver_loop(receiver: Receiver): + global _server_socket, _stop + try: + _server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + _server_socket.bind((HOST, PORT_IN)) + _server_socket.listen(1) + _server_socket.setblocking(False) + print(f"[Receiver] Listening on {HOST}:{PORT_IN}") + except Exception as e: + print("[Receiver] Bind failed:", e) + return + + buffer = "" + + while not _stop: + try: + readable, _, _ = select.select([_server_socket], [], [], 0.1) + if _server_socket in readable: + try: + conn, addr = _server_socket.accept() + conn.setblocking(False) + print("[Receiver] Connected:", addr) + + except Exception: + continue + + while not _stop: + try: + r, _, _ = select.select([conn], [], [], 0.05) + if conn in r: + chunk = conn.recv(1024) + if not chunk: + break + buffer += chunk.decode() + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + receiver.handle_message(line) + except Exception: + break + conn.close() + except Exception as e: + print("[Receiver] Loop error:", e) + print("[Receiver] Closing...") + try: + _server_socket.close() + except: + pass + _server_socket = None + + +def start(receiver: Receiver): + global _receiver_thread, _stop + if _receiver_thread and _receiver_thread.is_alive(): + return + _stop = False + _receiver_thread = threading.Thread(target=_receiver_loop, args=(receiver,) ,daemon=True) + _receiver_thread.start() + + +def stop(): + global _stop, _server_socket + _stop = True + if _server_socket: + _server_socket.close() diff --git a/BlenderExtension/see_blender_link/transport/sender.py b/BlenderExtension/see_blender_link/transport/sender.py new file mode 100644 index 00000000..7a69cce5 --- /dev/null +++ b/BlenderExtension/see_blender_link/transport/sender.py @@ -0,0 +1,30 @@ +import socket +import json +from ..config import HOST, PORT_OUT + +_blazor_socket = None + +def _connect(): + global _blazor_socket + try: + _blazor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _blazor_socket.connect((HOST, PORT_OUT)) + print("[Blender->Blazor] Connected") + except Exception as e: + print("[Blender->Blazor] Connect failed:", e) + _blazor_socket = None + + +def send_to_blazor(data: dict): + global _blazor_socket + + if not _blazor_socket: + _connect() + if not _blazor_socket: + return + + try: + msg = json.dumps(data) + "\n" + _blazor_socket.sendall(msg.encode("utf8")) + except Exception: + _blazor_socket = None diff --git a/BlenderExtension/see_blender_link/utils/__init__.py b/BlenderExtension/see_blender_link/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/utils/helper.py b/BlenderExtension/see_blender_link/utils/helper.py new file mode 100644 index 00000000..b9e7405e --- /dev/null +++ b/BlenderExtension/see_blender_link/utils/helper.py @@ -0,0 +1,28 @@ +import bpy +from mathutils import Vector +from bpy_extras.io_utils import axis_conversion + +def get_scene_scale(): + """Return diagonal of bounding box of all mesh objects.""" + meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH'] + if not meshes: + return 1.0 + + min_v = Vector((1e10, 1e10, 1e10)) + max_v = Vector((-1e10, -1e10, -1e10)) + + for obj in meshes: + for v in obj.bound_box: + w = obj.matrix_world @ Vector(v) + min_v = Vector((min(min_v[i], w[i]) for i in range(3))) + max_v = Vector((max(max_v[i], w[i]) for i in range(3))) + + return (max_v - min_v).length + +def renderer_to_blender_world(hit_render_pos): + # This must match your export axis conversion + global_matrix = axis_conversion( + to_forward="Z", + to_up="Y", + ).to_4x4() + return global_matrix.inverted() @ Vector(hit_render_pos) \ No newline at end of file diff --git a/SeeSharp.Blazor/GraphBrowser.razor b/SeeSharp.Blazor/GraphBrowser.razor new file mode 100644 index 00000000..e6115194 --- /dev/null +++ b/SeeSharp.Blazor/GraphBrowser.razor @@ -0,0 +1,92 @@ +@using SeeSharp.Integrators.Util + + +@namespace SeeSharp.Blazor +@if (Node != null) +{ +
+ + @if (HasChildren) + { + @(expanded ? "▼" : "▶") + } + else + { + + } + + + + @Node.GetType().Name + +
+ + @if (expanded && HasChildren) + { +
+ @foreach (var child in Node.Successors) + { + + } +
+ } + +} + +@code { + [Parameter] public PathGraphNode Node { get; set; } = default!; + [Parameter] public Action OnClick { get; set; } = default!; + [Parameter] public Action OnDoubleClick { get; set; } = default!; + [Parameter] public bool ForceExpand { get; set; } = false; + bool expanded = false; + bool HasChildren => Node.Successors?.Count > 0; + void Toggle() => expanded = !expanded; + + bool AutoExpand => + Node is ConnectionNode || Node is MergeNode || ForceExpand; + + protected override void OnParametersSet() + { + if (ForceExpand) + { + expanded = true; + } + } + void Click() + { + OnClick?.Invoke(Node); + } + + void DoubleClick() + { + OnDoubleClick?.Invoke(Node); + } + + string ToHex() + { + string color = "#000000"; + switch (Node.GetType().Name) + { + case "BSDFSampleNode": + color = "#FF0000"; //Red + break; + case "NextEventNode": + color = "#0000FF"; //Blue + break; + case "LightPathNode": + color = "#00AA00"; //Green + break; + case "ConnectionNode": + color = "#FFA500"; //Orange + break; + case "MergeNode": + color = "#00BFC4"; //Cyan + break; + case "BackgroundNode": + color = "#800080"; //Purple + break; + + } + return color; + } +} \ No newline at end of file diff --git a/SeeSharp.Blazor/GraphBrowser.razor.css b/SeeSharp.Blazor/GraphBrowser.razor.css new file mode 100644 index 00000000..6b647257 --- /dev/null +++ b/SeeSharp.Blazor/GraphBrowser.razor.css @@ -0,0 +1,26 @@ +.tree-node { + display: flex; + align-items: center; + font-family: monospace; +} + +.toggle { + width: 20px; + cursor: pointer; + user-select: none; +} + +.label { + cursor: pointer; + padding: 2px 4px; +} + +.label:hover { + background-color: #2d2d2d; +} + +.children { + margin-left: 20px; + border-left: 1px dotted #555; + padding-left: 6px; +} \ No newline at end of file diff --git a/SeeSharp.Blender/Addons/CursorTracker.cs b/SeeSharp.Blender/Addons/CursorTracker.cs new file mode 100644 index 00000000..9aee36ee --- /dev/null +++ b/SeeSharp.Blender/Addons/CursorTracker.cs @@ -0,0 +1,13 @@ +namespace SeeSharp.Blender +{ + public class CursorTrackerClient + { + public readonly CursorTrackedHandler _cursor_tracked; + public CursorTrackerClient(IEnumerable handlers) + { + _cursor_tracked = handlers + .OfType() + .Single(); // or First() + } + } +} diff --git a/SeeSharp.Blender/Addons/PathViewer.cs b/SeeSharp.Blender/Addons/PathViewer.cs new file mode 100644 index 00000000..75ca4397 --- /dev/null +++ b/SeeSharp.Blender/Addons/PathViewer.cs @@ -0,0 +1,22 @@ +namespace SeeSharp.Blender +{ + public class PathViewerClient + { + public readonly CreatedHandler _created; + public readonly DeletedHandler _deleted; + public readonly SelectedHandler _selected; + + public PathViewerClient(IEnumerable handlers) + { + _created = handlers + .OfType() + .Single(); // or First() + _deleted = handlers + .OfType() + .Single(); + _selected = handlers + .OfType() + .Single(); + } + } +} diff --git a/SeeSharp.Blender/BlenderCommandSender.cs b/SeeSharp.Blender/BlenderCommandSender.cs new file mode 100644 index 00000000..b432ce06 --- /dev/null +++ b/SeeSharp.Blender/BlenderCommandSender.cs @@ -0,0 +1,129 @@ +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +namespace SeeSharp.Blender +{ + public class BlenderCommandSender + { + private TcpClient _client; + private NetworkStream _stream; + private bool _connected = false; + + public async Task TryConnectAsync() + { + try + { + _client = new TcpClient(); + await _client.ConnectAsync("127.0.0.1", 5051); + _stream = _client.GetStream(); + _connected = true; + Console.WriteLine("✔ Connected to Blender cursor port"); + return true; + } + catch (Exception ex) + { + Console.WriteLine("❌ Blender not listening (connection failed): " + ex.Message); + _connected = false; + return false; + } + } + + public async Task SendCursorAsync(float x, float y, float z) + { + // Ensure connection exists + if (true) + { + bool ok = await TryConnectAsync(); + if (!ok) + { + // Do NOT crash — just gracefully fail + Console.WriteLine("⚠ Could not send cursor update, Blender not running"); + return; + } + } + + try + { + var data = new + { + cursor_position = new[] { x, y, z } + }; + + string json = JsonSerializer.Serialize(data) + "\n"; + byte[] bytes = Encoding.UTF8.GetBytes(json); + + await _stream.WriteAsync(bytes, 0, bytes.Length); + await _stream.FlushAsync(); + + Console.WriteLine("➡ Sent cursor update to Blender: " + json); + } + catch (Exception ex) + { + Console.WriteLine("❌ Error sending cursor data: " + ex.Message); + + // Force cleanup of dead connection + try { _stream?.Close(); } catch { } + try { _client?.Close(); } catch { } + + _stream = null; + _client = null; + _connected = false; + } + } + + public async Task SendCommandAsync(object cmd) + { + if (!_connected || !IsSocketConnected()) + { + _connected = false; + + Console.WriteLine("🔌 Lost connection — reconnecting…"); + bool ok = await TryConnectAsync(); + + if (!ok) + { + Console.WriteLine("⚠ Failed to reconnect"); + return; + } + } + try + { + string json = JsonSerializer.Serialize(cmd) + "\n"; + byte[] bytes = Encoding.UTF8.GetBytes(json); + + await _stream.WriteAsync(bytes, 0, bytes.Length); + await _stream.FlushAsync(); + + Console.WriteLine("➡ Sent command: " + json); + } + catch (Exception ex) + { + Console.WriteLine("❌ Error sending command: " + ex.Message); + _connected = false; + } + } + private bool IsSocketConnected() + { + try + { + if (_client == null || !_client.Connected) + return false; + + var s = _client.Client; + + // Check if the socket has been closed + bool readReady = s.Poll(0, SelectMode.SelectRead); + bool noBytes = (s.Available == 0); + + if (readReady && noBytes) + return false; + + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/BlenderEventDispatcher.cs b/SeeSharp.Blender/BlenderEventDispatcher.cs new file mode 100644 index 00000000..dfb346b3 --- /dev/null +++ b/SeeSharp.Blender/BlenderEventDispatcher.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace SeeSharp.Blender +{ + public interface IBlenderEventHandler +{ + string EventType { get; } + void Handle(JsonElement root); +} + +public class BlenderEventDispatcher +{ + private readonly Dictionary _handlers; + + public BlenderEventDispatcher(IEnumerable handlers) + { + _handlers = handlers.ToDictionary(h => h.EventType); + } + + public void Register(IBlenderEventHandler handler) + { + _handlers.Add(handler.EventType, handler); + } + + public void Unregister(IBlenderEventHandler handler) + { + _handlers.Remove(handler.EventType); + } + + public void Dispatch(JsonElement root) + { + if (!root.TryGetProperty("event", out var evtProp)) + return; + + var evt = evtProp.GetString(); + if (evt == null) + return; + + if (_handlers.TryGetValue(evt, out var handler)) + handler.Handle(root); + else + Console.WriteLine($"⚠️ Unknown Blender event: {evt}"); + } +} + +} \ No newline at end of file diff --git a/SeeSharp.Blender/BlenderEventListener.cs b/SeeSharp.Blender/BlenderEventListener.cs new file mode 100644 index 00000000..5f9ec2ec --- /dev/null +++ b/SeeSharp.Blender/BlenderEventListener.cs @@ -0,0 +1,66 @@ +using System.Net.Sockets; +using System.Net; +using System.Text.Json; +namespace SeeSharp.Blender +{ + public class BlenderEventListener +{ + private TcpListener? _listener; + private BlenderEventDispatcher _dispatcher; + + public BlenderEventListener(BlenderEventDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public void RegisterDispatcher(BlenderEventDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public async Task StartAsync() + { + _listener = new TcpListener(IPAddress.Loopback, 5052); + _listener.Start(); + Console.WriteLine("📡 Listening for Blender events on 5052..."); + + // while (true) + // { + // var client = await _listener.AcceptTcpClientAsync(); + // _ = HandleClientAsync(client); + // } + _ = Task.Run(async () => + { + while (true) + { + var client = await _listener.AcceptTcpClientAsync(); + Console.WriteLine("🔌 Blender connected to Blazor Event Listener"); + + _ = HandleClientAsync(client); + } + }); + } + + private async Task HandleClientAsync(TcpClient client) + { + using var reader = new StreamReader(client.GetStream()); + + while (true) + { + var line = await reader.ReadLineAsync(); + if (line == null) break; + Console.WriteLine("📨 Received event: " + line); + try + { + using var doc = JsonDocument.Parse(line); + _dispatcher.Dispatch(doc.RootElement); + } + catch (Exception ex) + { + Console.WriteLine("❌ Parse error: " + ex.Message); + } + } + } +} + +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/App.razor b/SeeSharp.Blender/Example/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/SeeSharp.Blender/Example/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply new file mode 100644 index 0000000000000000000000000000000000000000..56d35861fbd4180c541321e2cae35d4c43d1cf08 GIT binary patch literal 1184 zcmZXSzfTik7{?DN1y1o7D(gEq7>pO>s*~kTg*aebFeWA@^;+J_CD*%hJuC-NJ7SbX z7vsX;-+O}a&m#e$$^EzzW~3_`?k>cuF3ap`h5EQ`o3?~i}i94R-A|xUEc}g zg6Bq&#|zw_a~+@EFvj&#P^oY~V)sJsM0}1FV|LdA=ON1(H;o*7@MxA@XZ3pBD9X4I zgwJ*Eagp|dhY??56FI#a237osS=kHFa48vxlY!-Az)$7)sU$y@RZEtMj)yBdCC+Z0 zaL=tpY_T-&gly5p&T{yI3pYx`unf%C-JM(cM_cHOaAr!%lsAacT)#Hw#>Ed zu_v$a-<`=0@8qjo=6Cl`^M*xy6ZP+7K9y5_m7}kP3x|I4ZL~GslTRm z`$PG}S4BSY#w79I=%0mu>G~?~`Sr93e_Q>>XX?xzhPrhUSiMLY+`S$*TlmGNSh-1Gt7yB#s3x5(^z29_wTfDE1KOfMy zX|9>%lk6}3q5ky#R4)DPe!pq1`loW~Uj`;>Jy0*y2W2SI4`rbN3Jl^Hf^_H<1y17_ ahR#4|DR2(QdFTQ(LV=4oEVEnOt=b) z@(Y0MN`9y<)FVEnOt=b) z@(Y0MN`9yX#38|+y? NhA;v#6A&{4F#xWhss#W5 literal 0 HcmV?d00001 diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply new file mode 100644 index 0000000000000000000000000000000000000000..f3f69766a7bfc004a704c6ecaa49d6ee71df25cb GIT binary patch literal 458 zcmZXQzfQw25XSj8S&TfuZHR%0s_C4Hij55xrpQSzsU^pD9fve5bdf6jQi^?;W5JCpG%QDDB>BnPzV}%-FqnLGh)mc4IR#q)8y=P|Dk!gdmNVs z%Vz#{`~1F2pC(st!>!*nTHQBulQso$v%%}2y)q=Oi65OU$D96^*TDznzf%5TkGJkR N*lV|KsvW9bs&6d0s$l>C literal 0 HcmV?d00001 diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply new file mode 100644 index 0000000000000000000000000000000000000000..9b9c8bac5adae44324dc6f89bb62b567ba60a990 GIT binary patch literal 458 zcmXTOspLw_FUn0UQAoVEnOt=b) z@(Y0MN`9yU)*!RaK;&j%P;yI=5KkpEBs3Op75Xb z_6$&P|NNcl?#3eDVBf_b?Eo@ACZ``w9%Mm-Jqw6p L1Y#y2W(HyaL-eU% literal 0 HcmV?d00001 diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply new file mode 100644 index 0000000000000000000000000000000000000000..0939e11bfa28bc5459abb324a9abfd5c78fbb6bf GIT binary patch literal 458 zcmXTOspLw_FUn0UQAoVEnOt=b) z@(Y0MN`9ytT@*__P^`*J4fC# zW`Kh2?ZpmdmYW^4G2|QUK}vQ3l9Qx}VQ1yf48|*EC=Jx>2=faQ&>29!R0a1)V K%ml>DKnwtd>!OkX literal 0 HcmV?d00001 diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply new file mode 100644 index 0000000000000000000000000000000000000000..773daf8173bbde627a2e33b0138d6880a666a451 GIT binary patch literal 458 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

h^G&*B5Vif}!@D8$G7^ZMrBk?Hwn^PGFa^N-yA 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 @Body diff --git a/SeeSharp.Blender/Example/Pages/BlenderImporter.razor b/SeeSharp.Blender/Example/Pages/BlenderImporter.razor new file mode 100644 index 00000000..53dc5880 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/BlenderImporter.razor @@ -0,0 +1,119 @@ +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@inject ProtectedSessionStorage ProtectedSessionStore +@using SeeSharp.Experiments +@using SeeSharp.Blazor +@using System; +@using System.IO; + +@namespace SeeSharp.Blender +

+

Select a scene

+ +
+ +
+ @if (isSceneNameValid && !loading && scene?.Name != sceneNameInput.Text) + { + + } + else + { + + + @if (!loading) + { + + } + } +
+
+ +
+ Available scenes + +
+ + @if (loading) + { +

Loading...

+ } + else if (!string.IsNullOrEmpty(scene?.Name)) + { +

Loaded "@(scene.Name)"

+ } + +
+ +@code { + [Parameter] + public EventCallback OnSceneLoaded { get; set; } + + + IEnumerable availableSceneNames + { + get + { + if (_availableSceneNames == null) + _availableSceneNames = SceneRegistry.FindAvailableScenes().Order(); + return _availableSceneNames; + } + } + IEnumerable _availableSceneNames; + + public AutocompleteInput sceneNameInput { get; set; } + + SceneFromFile scene; + bool loading = false; + + SceneFromFile Scene => scene; + + bool isSceneNameValid => sceneNameInput?.Valid == true; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var result = await ProtectedSessionStore.GetAsync("lastScene"); + if (result.Success) + sceneNameInput.Text = result.Value; + } + } + + async Task OnSceneNameUpdate(string newName) + { + await ProtectedSessionStore.SetAsync("lastScene", newName); + } + + async Task LoadScene() + { + if (!isSceneNameValid || loading) return; + loading = true; + await Task.Run(() => scene = SceneRegistry.LoadScene(sceneNameInput.Text)); + loading = false; + await OnSceneLoaded.InvokeAsync(scene); + } + + public string FindJson(string folder) + { + folder = "Data/" + folder; + folder = Path.GetFullPath(folder); + var files = Directory.GetFiles(folder, "*.json"); + if (files.Length == 0) + { + Console.WriteLine("Error: No JSON file found in the folder."); + return null; + } + + return Path.GetFullPath(files[0]); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css b/SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css new file mode 100644 index 00000000..7dd987e3 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css @@ -0,0 +1,66 @@ +::deep .scene-button { + margin-top: 0px; + margin-bottom: 2px; + background-color: #cfe8ef; + color: rgb(12, 12, 12); + padding-left: 4px; + padding-top: 2px; + padding-bottom: 2px; + overflow: hidden; +} + +::deep .scene-button:hover { + cursor: pointer; + background-color: #8edeee; +} + +.dropdown-header { + font-weight: bold; +} + +.dropdown-header:hover { + cursor: pointer; +} + +.dropdown-body { + column-count: auto; + column-width: 12rem; + margin-left: 8px; + border-left-width: 8px; + border-left-color: #208dab; + border-left-style: solid; + background-color: #cfe8ef; +} + +.scene-picker { + background-color: #e6f1f4; + padding: 8px; + + border-style: solid; + border-width: 1pt; + border-color: #2b3f44; +} + +.btn { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + +.btn:hover { + background-color: #c9eff4; + cursor: pointer; +} + +.btn:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/CursorTracker.razor b/SeeSharp.Blender/Example/Pages/CursorTracker.razor new file mode 100644 index 00000000..91bce976 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/CursorTracker.razor @@ -0,0 +1,44 @@ +@page "/CursorTracker" +@using SeeSharp.Blender +@inject SeeSharp.Blender.CursorTrackerClient Client +@inject SeeSharp.Blender.BlenderCommandSender Commander +@inject SeeSharp.Blender.BlenderEventListener Listener +

Blender Live Data

+ +@if (_data is null) +{ +

Waiting for Blender...

+} +else +{ +
+ @if (_data.Hit_Position != null) + { +

Cursor: @Format(_data.Cursor_Position)

+

Object: @_data.Object

+

Hit Position: @Format(_data.Hit_Position)

+

Normal: @Format(_data.Normal)

+

Face Index: @_data.Face_Index

+ } + else + { +

No object hit

+ } +
+} + +@code { + private BlenderCursorData? _data; + protected override void OnInitialized() + { + Client._cursor_tracked.OnCursorTracked += data => + { + Console.WriteLine("RAW JSON: " + JsonSerializer.Serialize(data)); + _data = data; + InvokeAsync(StateHasChanged); + }; + } + + string Format(float[]? arr) + => arr == null ? "null" : string.Join(", ", arr); +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/Experiment.razor b/SeeSharp.Blender/Example/Pages/Experiment.razor new file mode 100644 index 00000000..76543c49 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/Experiment.razor @@ -0,0 +1,97 @@ +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor + +@inject IJSRuntime JS + +@page "/Experiment" + +

Example experiment

+ +
+
+ +
+ +
+ +
+
+ @if (readyToRun) + { +

+ } + + + +
+ + @if (!running) + { + @if (resultsAvailable) + { +
+ + + @if (selected.HasValue && selected.Value) + { + + + + + +
Mesh@(selected.Value.Mesh.Name)
Material@(selected.Value.Mesh.Material.Name) (roughness: @(selected.Value.Mesh.Material.GetRoughness(selected.Value)), transmissive: @(selected.Value.Mesh.Material.IsTransmissive(selected.Value)))
Distance@(selected.Value.Distance)
Position@(selected.Value.Position)
+ } + +
+ } + } + else + { +

Rendering...

+ } +
+ + + +@code { + SceneSelector sceneSelector; + Scene scene; + bool readyToRun = false; + bool running = false; + bool sceneJustLoaded = false; + bool resultsAvailable = false; + ElementReference runButton; + + SimpleImageIO.FlipBook flip; + + async Task OnSceneLoaded(SceneFromFile sceneFromFile) + { + await Task.Run(() => scene = sceneFromFile.MakeScene()); + flip = null; + resultsAvailable = false; + readyToRun = true; + sceneJustLoaded = true; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (readyToRun && sceneJustLoaded) + { + await runButton.FocusAsync(); + } + + sceneJustLoaded = false; + } + + async Task OnRunClick() + { + readyToRun = false; + resultsAvailable = false; + running = true; + await Task.Run(() => RunExperiment()); + readyToRun = true; + running = false; + resultsAvailable = true; + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/Experiment.razor.cs b/SeeSharp.Blender/Example/Pages/Experiment.razor.cs new file mode 100644 index 00000000..4cecd73c --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/Experiment.razor.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Components; +using SeeSharp.Blazor; + +namespace SeeSharp.Blender.Example.Pages; + +public partial class Experiment : ComponentBase +{ + const int Width = 1280; + const int Height = 720; + const int MaxDepth = 10; + + int NumSamples = 2; + + long renderTimePT, renderTimeVCM; + + void RunExperiment() + { + flip = new FlipBook(660, 580) + .SetZoom(FlipBook.InitialZoom.FillWidth) + .SetToneMapper(FlipBook.InitialTMO.Exposure(scene.RecommendedExposure)) + .SetToolVisibility(false); + + scene.FrameBuffer = new(Width, Height, ""); + scene.Prepare(); + + PathTracer pathTracer = new() + { + TotalSpp = NumSamples, + MaxDepth = MaxDepth, + }; + pathTracer.Render(scene); + flip.Add($"PT", scene.FrameBuffer.Image); + renderTimePT = scene.FrameBuffer.RenderTimeMs; + + scene.FrameBuffer = new(Width, Height, ""); + VertexConnectionAndMerging vcm = new() + { + NumIterations = NumSamples, + MaxDepth = MaxDepth, + }; + vcm.Render(scene); + flip.Add($"VCM", scene.FrameBuffer.Image); + renderTimeVCM = scene.FrameBuffer.RenderTimeMs; + } + + SurfacePoint? selected; + + void OnFlipClick(FlipViewer.OnClickEventArgs args) + { + if (args.CtrlKey) + { + selected = scene.RayCast(new(args.X, args.Y)); + } + } + + async Task OnDownloadClick() + { + HtmlReport report = new(); + report.AddMarkdown(""" + # Example experiment + $$ L_\mathrm{o} = \int_\Omega L_\mathrm{i} f_\mathrm{r} |\cos\theta_\mathrm{i}| \, d\omega_\mathrm{i} $$ + """); + report.AddFlipBook(flip); + await SeeSharp.Blazor.Scripts.DownloadAsFile(JS, "report.html", report.ToString()); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/Index.razor b/SeeSharp.Blender/Example/Pages/Index.razor new file mode 100644 index 00000000..9bbea8ef --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/Index.razor @@ -0,0 +1,39 @@ +@page "/" + +@using System.Reflection +@using System.Text.RegularExpressions + + +
+ +
+ + +@code { + /// Enumerates all .razor components in this folder + public IEnumerable<(string Name, string Url)> GetExperimentPages() + { + var routableComponents = Assembly + .GetExecutingAssembly() + .ExportedTypes + .Where(t => t.IsSubclassOf(typeof(ComponentBase))) + .Where(c => c + .GetCustomAttributes(inherit: true) + .OfType() + .Count() > 0); + + foreach (var routableComponent in routableComponents) + { + string name = routableComponent.ToString().Replace("SeeSharp.Blender.Example.Pages.", string.Empty); + if (name != "Index") + yield return (name, name); + } + } +} diff --git a/SeeSharp.Blender/Example/Pages/PathViewer.razor b/SeeSharp.Blender/Example/Pages/PathViewer.razor new file mode 100644 index 00000000..92a8f5d8 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/PathViewer.razor @@ -0,0 +1,232 @@ +@page "/PathViewer" +@inject SeeSharp.Blender.PathViewerClient Client +@inject SeeSharp.Blender.BlenderCommandSender Commander +@inject SeeSharp.Blender.BlenderEventListener Listener + +@using Markdig.Extensions.SelfPipeline +@using Microsoft.AspNetCore.Routing.Internal +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor +@using SeeSharp.Common; +@using SeeSharp.Integrators; +@using SeeSharp.Integrators.Bidir; +@using SeeSharp.Blender; +@inject IJSRuntime JS +

Render Scene

+ +
+
+ +
+ +
+ + + + +
    + @foreach (var path in paths) + { +
  • + @path + + +
  • + @if (path == SelectedId) + { + + } + } +
+ +@if (renderedImage != null) +{ + + +
+

Rendered Image

+ +
+} + +@code { + BlenderImporter sceneSelector; + private string renderedImage; + private string hitInfo; + + private List paths = new(); + private string? SelectedId = null; + public PathGraphNode? viewer_root = null; + public Dictionary? viewer_roots = new Dictionary(); + + Scene scene; + Integrator path_tracer = new PathTracer() + { + MaxDepth = 5, + TotalSpp = 1, + EnableDenoiser = false + }; + + Integrator bi_path_tracer = new CameraStoringVCM() + { + NumIterations = 10, + MaxDepth = 10, + EnableDenoiser = false + }; + Integrator integrator = new PathTracer() + { + MaxDepth = 5, + TotalSpp = 1, + EnableDenoiser = false + }; + + void OnModeChanged(ChangeEventArgs e) + { + var SelectedMode = e.Value?.ToString() ?? ""; + + // Update ModeExecutor based on SelectedMode + integrator = SelectedMode switch + { + "PathTracer" => path_tracer, + "Bidir" => bi_path_tracer, + _ => path_tracer + }; + } + PathGraph graph; + RgbColor estimate; + + async Task OnSceneLoaded(SceneFromFile sceneFromFile) + { + await Task.Run(() => scene = sceneFromFile.MakeScene()); + Console.WriteLine("About to load this scene: " + sceneSelector.FindJson(sceneSelector.sceneNameInput.Text)); + + paths.Clear(); + viewer_roots.Clear(); + + await Commander.SendCommandAsync(new + { + command = "import_scene", + scene_name = sceneSelector.FindJson(sceneSelector.sceneNameInput.Text) + }); + } + + async Task Render() + { + scene.FrameBuffer = new(1920, 1080, "../Data/Render.exr"); + scene.Prepare(); + path_tracer.Render(scene); + scene.FrameBuffer.Image.WriteToFile("./Data/Render.png"); + var bytes = File.ReadAllBytes("./Data/Render.png"); + renderedImage = $"data:image/png;base64,{Convert.ToBase64String(bytes)}"; + } + + protected override void OnInitialized() + { + Client._created.OnCreated += obj => + { + paths.Add(obj); + SelectedId = null; + InvokeAsync(StateHasChanged); + }; + Client._deleted.OnDeleted += obj => + { + paths.Remove(obj); + viewer_roots.Remove(obj); + InvokeAsync(StateHasChanged); + }; + Client._selected.OnSelected += obj => + { + SelectedId = obj; + InvokeAsync(StateHasChanged); + }; + } + + async Task OnImageClick(MouseEventArgs e) + { + var pt = await JS.InvokeAsync("getBlenderPixelCoords", + "renderImg", e.ClientX, e.ClientY); + + scene.FrameBuffer = new(1920, 1080, "../Data/Render.exr"); + scene.Prepare(); + (graph, estimate) = integrator.ReplayPixel(scene, new Pixel((int)pt.x, (int)pt.y), 0); + PathGraphNode node = graph.Roots[0]; + viewer_root = graph.Roots[0]; + string jsonGraph = GraphSerializer.SerializeGraph(node); + string path_id = Guid.NewGuid().ToString(); + viewer_roots.Add(path_id, graph.Roots[0]); + await Commander.SendCommandAsync(new + { + command = "create_path", + id = path_id, + graph = jsonGraph + }); + } + + async Task DeletePath(string id) + { + await Commander.SendCommandAsync(new { command = "delete_path", id }); + } + + async Task SelectPath(string id) + { + await Commander.SendCommandAsync(new { command = "select_path", id }); + } + public class PixelPos { public float x { get; set; } public float y { get; set; } } + + async void OnNodeClicked(PathGraphNode node) + { + var node_list = new List(); + for (var n = node; n != null; n = n.Ancestor) + { + node_list.Add(n.GetHashCode().ToString("X")); + } + + node_list.Reverse(); + bool is_full_graph = (node == viewer_roots[SelectedId]); + + await Commander.SendCommandAsync(new + { + command = "click_on_node", + path = JsonSerializer.Serialize(node_list, new + JsonSerializerOptions + { + WriteIndented = false + }), + path_id = SelectedId, + is_full_graph + }); + } + async void OnNodeDoubleClicked(PathGraphNode node) + { + var node_list = new List(); + for (var n = node; n != null; n = n.Ancestor) + { + node_list.Add(n.GetHashCode().ToString("X")); + } + + node_list.Reverse(); + bool is_full_graph = (node == viewer_roots[SelectedId]); + await Commander.SendCommandAsync(new + { + command = "dbclick_on_node", + path = JsonSerializer.Serialize(node_list, new + JsonSerializerOptions + { + WriteIndented = false + }), + path_id = SelectedId, + is_full_graph + }); + } + string GetStyle(string id) + { + return id == SelectedId + ? "background-color: yellow; font-weight: bold;" + : ""; + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/Pages/PathViewer.razor.cs b/SeeSharp.Blender/Example/Pages/PathViewer.razor.cs new file mode 100644 index 00000000..b56ed6f8 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/PathViewer.razor.cs @@ -0,0 +1,214 @@ +using System.Text.Json; + +public record Vec3DTO(float X, float Y, float Z); +public record Vec2DTO(float X, float Y); + +public record SurfacePointDTO(Vec3DTO Position, Vec3DTO Normal, Vec2DTO BarycentricCoords, + Mesh Mesh, uint PrimId, float ErrorOffset, + float Distance, Vec3DTO ShadingNormal, Vec2DTO TextureCoordinates, Material Material); + +public record PathVertexDTO(SurfacePointDTO Point, float PdfFromAncestor, float PdfReverseAncestor, + float PdfNextEventAncestor, Vec3DTO DirToAncestor, float JacobianToAncestor, + RgbColor Weight, int PathId, byte Depth, float MaximumRoughness, bool FromBackground); +public class NodeDTO +{ + public string NodeType { get; set; } + public Vec3DTO Position { get; set; } + public Dictionary Data { get; set; } = new(); +} + +public static class GraphNodeSerializer +{ + public static NodeDTO Serialize(PathGraphNode node) + { + return node switch + { + NextEventNode n => SerializeNextEventNode(n), + BSDFSampleNode n => SerializeBSDFSampleNode(n), + LightPathNode n => SerializeLightPathNode(n), + ConnectionNode n => SerializeConnectionNode(n), + MergeNode n => SerializeMergeNode(n), + BackgroundNode n => SerializeBackgroundNode(n), + _ => SerializeBase(node) + }; + } + + public static Vec3DTO ToDTO(this Vector3 v) + => new(v.X, v.Y, v.Z); + + public static Vec2DTO ToDTO(this Vector2 v) + => new(v.X, v.Y); + public static SurfacePointDTO toDTO(this SurfacePoint s) + => new(s.Position.ToDTO(), s.Normal.ToDTO(), s.BarycentricCoords.ToDTO(), + s.Mesh, s.PrimId, s.ErrorOffset, s.Distance, s.ShadingNormal.ToDTO(), s.TextureCoordinates.ToDTO(), s.Material); + + public static PathVertexDTO toDTO(this PathVertex p) + => new(p.Point.toDTO(), p.PdfFromAncestor, p.PdfReverseAncestor, p.PdfNextEventAncestor, p.DirToAncestor.ToDTO(), + p.JacobianToAncestor, p.Weight, p.PathId, p.Depth, p.MaximumRoughness, p.FromBackground); + private static NodeDTO SerializeBase(PathGraphNode n) + { + return new NodeDTO + { + NodeType = n.GetType().Name, + Position = n.Position.ToDTO(), + Data = { + ["IsBackground"] = n.IsBackground, + ["SuccessorCount"] = n.Successors.Count + } + }; + } + + private static NodeDTO SerializeNextEventNode(NextEventNode n) + { + return new NodeDTO + { + NodeType = "NextEventNode", + Position = n.Position.ToDTO(), + Data = { + ["Emission"] = n.Emission, + ["Pdf"] = n.Pdf, + ["BsdfTimesCosine"] = n.BsdfTimesCosine, + ["MISWeight"] = n.MISWeight, + ["PrefixWeight"] = n.PrefixWeight, + ["HasSurfacePoint"] = n.Point != null + } + }; + } + + private static NodeDTO SerializeBSDFSampleNode(BSDFSampleNode n) + { + return new NodeDTO + { + NodeType = "BSDFSampleNode", + Position = n.Position.ToDTO(), + Data = { + ["ScatterWeight"] = n.ScatterWeight, + ["SurvivalProbability"] = n.SurvivalProbability, + ["Emission"] = n.Emission, + ["MISWeight"] = n.MISWeight, + ["SurfacePoint"] = n.Point.toDTO() + } + }; + } + + private static NodeDTO SerializeLightPathNode(LightPathNode n) + { + return new NodeDTO + { + NodeType = "LightPathNode", + Position = n.Position.ToDTO(), + Data = { + ["LightVertex"] = n.LightVertex.toDTO() + } + }; + } + + private static NodeDTO SerializeConnectionNode(ConnectionNode n) + { + return new NodeDTO + { + NodeType = "ConnectionNode", + Position = n.Position.ToDTO(), + Data = { + ["Contrib"] = n.Contrib, + ["MISWeight"] = n.MISWeight, + ["LightVertex"] = n.LightVertex.toDTO() + } + }; + } + + private static NodeDTO SerializeMergeNode(MergeNode n) + { + return new NodeDTO + { + NodeType = "MergeNode", + Position = n.Position.ToDTO(), + Data = { + ["Contrib"] = n.Contrib, + ["MISWeight"] = n.MISWeight, + ["LightVertex"] = n.LightVertex.toDTO() + } + }; + } + + private static NodeDTO SerializeBackgroundNode(BackgroundNode n) + { + return new NodeDTO + { + NodeType = "BackgroundNode", + Position = n.Position.ToDTO(), + Data = { + ["Emission"] = n.Emission, + ["MISWeight"] = n.MISWeight + } + }; + } +} + +public static class NodeIdGenerator +{ + public static string GetId(PathGraphNode node) + => node.GetHashCode().ToString("X"); // stable enough for session +} + +public static class GraphSerializer +{ + public static string SerializeGraph(PathGraphNode root) + { + var visited = new HashSet(); + var nodes = new List>(); + var edges = new List<(string, string)>(); + + Traverse(root, visited, nodes, edges); + + var final = new + { + nodes, + edges = edges.Select(e => new[] { e.Item1, e.Item2 }) + }; + + return JsonSerializer.Serialize(final, new JsonSerializerOptions + { + WriteIndented = false + }); + } + + private static void Traverse( + PathGraphNode node, + HashSet visited, + List> nodes, + List<(string, string)> edges) + { + if (node == null || visited.Contains(node)) + return; + + visited.Add(node); + + string id = NodeIdGenerator.GetId(node); + var dto = GraphNodeSerializer.Serialize(node); + + // Convert DTO into raw dictionary for JSON + var nodeDict = new Dictionary + { + ["id"] = id, + ["type"] = dto.NodeType, + ["position"] = dto.Position, // Vec3DTO (serializable) + ["data"] = dto.Data // Dictionary + }; + + nodes.Add(nodeDict); + + // Traverse successors + foreach (var child in node.Successors) + { + if (child != null) + { + string childId = NodeIdGenerator.GetId(child); + edges.Add((id, childId)); + } + + Traverse(child, visited, nodes, edges); + } + } +} + diff --git a/SeeSharp.Blender/Example/Pages/_Host.cshtml b/SeeSharp.Blender/Example/Pages/_Host.cshtml new file mode 100644 index 00000000..db74ad21 --- /dev/null +++ b/SeeSharp.Blender/Example/Pages/_Host.cshtml @@ -0,0 +1,35 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace SeeSharp.Blender.Example.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + @Html.Raw(SeeSharp.Blazor.Scripts.AllScripts) + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + + diff --git a/SeeSharp.Blender/Example/Program.cs b/SeeSharp.Blender/Example/Program.cs new file mode 100644 index 00000000..492d502f --- /dev/null +++ b/SeeSharp.Blender/Example/Program.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +ProgressBar.Silent = true; +SceneRegistry.AddSourceRelativeToScript("./Data"); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +var eventListener = app.Services.GetRequiredService(); +_ = eventListener.StartAsync(); + +if (!app.Environment.IsDevelopment()) +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); \ No newline at end of file diff --git a/SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj b/SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj new file mode 100644 index 00000000..3f177c5f --- /dev/null +++ b/SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + + diff --git a/SeeSharp.Blender/Example/_Imports.razor b/SeeSharp.Blender/Example/_Imports.razor new file mode 100644 index 00000000..b5601de5 --- /dev/null +++ b/SeeSharp.Blender/Example/_Imports.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using SeeSharp.Blender.Example + +@using SeeSharp.Blazor diff --git a/SeeSharp.Blender/Example/appsettings.Development.json b/SeeSharp.Blender/Example/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/SeeSharp.Blender/Example/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/SeeSharp.Blender/Example/appsettings.json b/SeeSharp.Blender/Example/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/SeeSharp.Blender/Example/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/SeeSharp.Blender/Example/wwwroot/css/site.css b/SeeSharp.Blender/Example/wwwroot/css/site.css new file mode 100644 index 00000000..ddc98cca --- /dev/null +++ b/SeeSharp.Blender/Example/wwwroot/css/site.css @@ -0,0 +1,86 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 3.5rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +html { + font-family: system-ui; +} + +button { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + button:hover { + background-color: #c9eff4; + cursor: pointer; + } + button:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; + } + +.experiment-settings { + display: flex; + gap: 0.25em; + flex-direction: column; + float: left; + margin-right: 1em; +} + +.experiment-results { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: flex-start; +} + +table { + border-collapse: collapse; +} +td, th { + border: none; + padding: 4px; +} +tr:hover { background-color: #e7f2f1; } +th { + padding-top: 6px; + padding-bottom: 6px; + text-align: left; + background-color: #4a96af; + color: white; + font-size: smaller; +} \ No newline at end of file diff --git a/SeeSharp.Blender/Example/wwwroot/raycast.js b/SeeSharp.Blender/Example/wwwroot/raycast.js new file mode 100644 index 00000000..2bd8ffc0 --- /dev/null +++ b/SeeSharp.Blender/Example/wwwroot/raycast.js @@ -0,0 +1,23 @@ +window.getBlenderPixelCoords = function (imgId, clickX, clickY) { + const img = document.getElementById(imgId); + if (!img) return null; + + const rect = img.getBoundingClientRect(); + + // Pixel inside displayed image + const x_ui = clickX - rect.left; + const y_ui = clickY - rect.top; + + // Displayed size + const Wu = rect.width; + const Hu = rect.height; + + // Blender render resolution + const Wb = img.naturalWidth; + const Hb = img.naturalHeight; + + return { + x: x_ui / Wu * Wb, + y: y_ui / Hu * Hb + }; +}; \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/CreatedHandler.cs b/SeeSharp.Blender/Handlers/CreatedHandler.cs new file mode 100644 index 00000000..261f4ec7 --- /dev/null +++ b/SeeSharp.Blender/Handlers/CreatedHandler.cs @@ -0,0 +1,13 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class CreatedHandler : IBlenderEventHandler +{ + public string EventType => "created"; + + public event Action? OnCreated; + + public void Handle(JsonElement root) + { + OnCreated?.Invoke(root.GetProperty("id").GetString()); + } +} diff --git a/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs b/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs new file mode 100644 index 00000000..78824a5f --- /dev/null +++ b/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs @@ -0,0 +1,38 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class BlenderCursorData + { + // [JsonPropertyName("object")] + public string? Object { get; set; } + // [JsonPropertyName("cursor_position")] + public float[] Cursor_Position { get; set; } = Array.Empty(); + // [JsonPropertyName("hit_position")] + public float[]? Hit_Position { get; set; } + // [JsonPropertyName("face_index")] + public int? Face_Index { get; set; } + // [JsonPropertyName("normal")] + public float[]? Normal { get; set; } + } + +public class CursorTrackedHandler : IBlenderEventHandler +{ + public string EventType => "cursor_tracked"; + public event Action? OnCursorTracked; + + public void Handle(JsonElement root) + { + OnCursorTracked?.Invoke(new BlenderCursorData + { + Object = root.TryGetProperty("object", out var obj) ? + obj.GetString() : null, + Cursor_Position = + JsonSerializer.Deserialize(root.GetProperty("cursor_position")), + Hit_Position = root.TryGetProperty("hit_position", out var pos) ? + JsonSerializer.Deserialize(pos.GetRawText()) : null, + Face_Index = root.TryGetProperty("face_index", out var face) ? + face.GetInt32() : null, + Normal = root.TryGetProperty("normal", out var norm) ? + JsonSerializer.Deserialize(norm.GetRawText()) : null, + }); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/DeletedHandler.cs b/SeeSharp.Blender/Handlers/DeletedHandler.cs new file mode 100644 index 00000000..0e419a72 --- /dev/null +++ b/SeeSharp.Blender/Handlers/DeletedHandler.cs @@ -0,0 +1,12 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class DeletedHandler : IBlenderEventHandler +{ + public string EventType => "deleted"; + public event Action? OnDeleted; + + public void Handle(JsonElement root) + { + OnDeleted?.Invoke(root.GetProperty("id").GetString()); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/SelectedHandler.cs b/SeeSharp.Blender/Handlers/SelectedHandler.cs new file mode 100644 index 00000000..06245874 --- /dev/null +++ b/SeeSharp.Blender/Handlers/SelectedHandler.cs @@ -0,0 +1,12 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class SelectedHandler : IBlenderEventHandler +{ + public string EventType => "selected"; + public event Action? OnSelected; + + public void Handle(JsonElement root) + { + OnSelected?.Invoke(root.GetProperty("id").GetString()); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/SeeSharp.Blender.csproj b/SeeSharp.Blender/SeeSharp.Blender.csproj new file mode 100644 index 00000000..f773e381 --- /dev/null +++ b/SeeSharp.Blender/SeeSharp.Blender.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + \ No newline at end of file From 6520f74b3a65dafe334a10102b1e6a6b880a0f57 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Tue, 6 Jan 2026 15:51:51 +0100 Subject: [PATCH 02/13] add SeeSharp.Blender project to .sln --- SeeSharp.sln | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/SeeSharp.sln b/SeeSharp.sln index c0161e52..68ee0080 100644 --- a/SeeSharp.sln +++ b/SeeSharp.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blazor", "SeeSharp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest", "MaterialTest\MaterialTest.csproj", "{5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blender", "SeeSharp.Blender\SeeSharp.Blender.csproj", "{BB86935A-DF0E-4B98-9823-41A15F85BD7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,9 +36,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -170,5 +169,20 @@ Global {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x64.Build.0 = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.ActiveCfg = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x64.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x86.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|Any CPU.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x64.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x64.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal From 33dcad9e20cf87d493b692da359a901b6cc7c4ba Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 13 Jan 2026 11:13:51 +0100 Subject: [PATCH 03/13] using ComputeVisualizerColor() --- SeeSharp.Blazor/GraphBrowser.razor | 35 +++++------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/SeeSharp.Blazor/GraphBrowser.razor b/SeeSharp.Blazor/GraphBrowser.razor index e6115194..fd9bada3 100644 --- a/SeeSharp.Blazor/GraphBrowser.razor +++ b/SeeSharp.Blazor/GraphBrowser.razor @@ -16,7 +16,10 @@ } - + @Node.GetType().Name @@ -56,37 +59,9 @@ { OnClick?.Invoke(Node); } - + void DoubleClick() { OnDoubleClick?.Invoke(Node); } - - string ToHex() - { - string color = "#000000"; - switch (Node.GetType().Name) - { - case "BSDFSampleNode": - color = "#FF0000"; //Red - break; - case "NextEventNode": - color = "#0000FF"; //Blue - break; - case "LightPathNode": - color = "#00AA00"; //Green - break; - case "ConnectionNode": - color = "#FFA500"; //Orange - break; - case "MergeNode": - color = "#00BFC4"; //Cyan - break; - case "BackgroundNode": - color = "#800080"; //Purple - break; - - } - return color; - } } \ No newline at end of file From 564cd15967fe4973c26f7863228509c72f222f60 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 13 Jan 2026 11:48:41 +0100 Subject: [PATCH 04/13] fix extension building --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index eebeff0b..75875147 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ _blender_binaries: # Builds the Blender add-on .zip [working-directory: "./BlenderExtension/see_blender/"] blender: _build_dotnet _blender_binaries - blender --command extension build --output-dir .. + blender --factory-startup --command extension build --output-dir .. @echo "" @echo "Blender plugin built. Open Blender and go to 'Edit - Preferences - Addons - Install from Disk' (dropdown menu in the top-right corner)" @echo "Browse to the 'BlenderExtension/see_sharp_renderer-VERSION.zip' file in this directory and install it." From 1ef68065e1b9ed4eebc0bd0d935fcfdfd459ee17 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 13 Jan 2026 14:15:32 +0100 Subject: [PATCH 05/13] fixing material, mesh import function + modify cornell json file --- BlenderExtension/see_blender/importer.py | 113 +- Data/Scenes/CornellBox/CornellBox.json | 1905 ++++++++++++---------- 2 files changed, 1163 insertions(+), 855 deletions(-) diff --git a/BlenderExtension/see_blender/importer.py b/BlenderExtension/see_blender/importer.py index 4f85c99f..34a2081e 100644 --- a/BlenderExtension/see_blender/importer.py +++ b/BlenderExtension/see_blender/importer.py @@ -3,6 +3,7 @@ import math import bpy import mathutils +import bmesh from bpy_extras.io_utils import axis_conversion # ------------------------------------------------------------------------ @@ -48,7 +49,7 @@ def make_material(name, mat_json, base_path): links.new(tex.outputs["Color"], principled.inputs["Base Color"]) # -------- Roughness (texture or float) - roughness = mat_json["roughness"] + roughness = mat_json.get("roughness", 1.0) if isinstance(roughness, str): # texture img_path = os.path.join(base_path, roughness) img = load_image(img_path) @@ -66,8 +67,8 @@ def make_material(name, mat_json, base_path): principled.inputs["Anisotropic"].default_value = mat_json.get("anisotropic", 0.0) # Emission - emission_json = mat_json["emission"] - if emission_json["type"] == "rgb": + emission_json = mat_json.get("emission") + if emission_json and emission_json.get("type") == "rgb": color = mat_json["emission_color"]["value"] principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) principled.inputs["Emission Strength"].default_value = mat_json["emission_strength"] @@ -88,6 +89,68 @@ def load_ply(filepath): return new_objs[0] return None +def import_trimesh_object(obj, mat_lookup): + name = obj.get("name", "Trimesh") + + mesh = bpy.data.meshes.new(name) + bm = bmesh.new() + + verts = obj["vertices"] + indices = obj["indices"] + + # ---- vertices + bm_verts = [] + for i in range(0, len(verts), 3): + bm_verts.append(bm.verts.new((verts[i], verts[i+1], verts[i+2]))) + bm.verts.ensure_lookup_table() + + # ---- faces + for i in range(0, len(indices), 3): + try: + bm.faces.new(( + bm_verts[indices[i]], + bm_verts[indices[i+1]], + bm_verts[indices[i+2]], + )) + except ValueError: + pass + + bm.to_mesh(mesh) + bm.free() + + obj_bl = bpy.data.objects.new(name, mesh) + bpy.context.collection.objects.link(obj_bl) + + # ---- normals (optional) + if "normals" in obj: + normals = obj["normals"] + loop_normals = [] + + for loop in mesh.loops: + vi = loop.vertex_index + loop_normals.append(mathutils.Vector(normals[vi*3 : vi*3 + 3])) + + mesh.normals_split_custom_set(loop_normals) + + # ---- UVs (optional) + if "uv" in obj: + uv_layer = mesh.uv_layers.new(name="UVMap") + uvs = obj["uv"] + for poly in mesh.polygons: + for loop_idx in poly.loop_indices: + vi = mesh.loops[loop_idx].vertex_index + uv_layer.data[loop_idx].uv = ( + uvs[vi*2], + uvs[vi*2 + 1] + ) + + # ---- material + mat_name = obj.get("material") + if mat_name in mat_lookup: + mesh.materials.append(mat_lookup[mat_name]) + + return obj_bl + # ------------------------------------------------------------------------ # Camera @@ -186,18 +249,40 @@ def import_seesharp(filepath): if "objects" in data: for obj in data["objects"]: - ply_path = os.path.join(base_path, obj["relativePath"]) - new_obj = load_ply(ply_path) - if not new_obj: - print(f"Failed to load {ply_path}") - continue - - # Apply SeeSharp → Blender inverse transform + if obj.get("type") == "trimesh": + new_obj = import_trimesh_object(obj, mat_lookup) + else: + # fallback to existing PLY logic + ply_path = os.path.join(base_path, obj["relativePath"]) + new_obj = load_ply(ply_path) + if not new_obj: + print(f"Failed to load {ply_path}") + continue + + if obj.get("material") in mat_lookup: + new_obj.data.materials.append(mat_lookup[obj["material"]]) + # Apply transform (shared) new_obj.matrix_world = global_matrix.inverted() - - # Assign material - if obj["material"] in mat_lookup: - new_obj.data.materials.append(mat_lookup[obj["material"]]) + # for obj in data["objects"]: + # # ply_path = os.path.join(base_path, obj["relativePath"]) + # rel_path = obj.get("relativePath") + + # if not rel_path: + # print(f"⚠ Skipping object '{obj.get('name', 'UNKNOWN')}', no relativePath") + # continue + + # ply_path = os.path.join(base_path, rel_path) + # new_obj = load_ply(ply_path) + # if not new_obj: + # print(f"Failed to load {ply_path}") + # continue + + # # Apply SeeSharp → Blender inverse transform + # new_obj.matrix_world = global_matrix.inverted() + + # # Assign material + # if obj.get("material") in mat_lookup: + # new_obj.data.materials.append(mat_lookup[obj["material"]]) bpy.context.scene.render.engine = "SEE_SHARP" diff --git a/Data/Scenes/CornellBox/CornellBox.json b/Data/Scenes/CornellBox/CornellBox.json index 40b19015..ca726cfd 100644 --- a/Data/Scenes/CornellBox/CornellBox.json +++ b/Data/Scenes/CornellBox/CornellBox.json @@ -1,880 +1,1103 @@ { - "name": "Cornell Box", - "transforms": [ - { - "name": "camera", - "position": [ 0.0, 1.0, 6.8 ], - "rotation": [ 0.0, 0.0, 0.0 ], - "scale": [ 1.0, 1.0, 1.0 ] - } - ], - "cameras": [ - { - "name": "default", - "type": "perspective", - "fov": 19.5, - "transform": "camera" - } - ], - "materials": [ - { - "name": "LeftWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.63, 0.065, 0.05] - } - }, - { - "name": "RightWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.14, 0.45, 0.091] - } - }, - { - "name": "Floor", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "Ceiling", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "BackWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "ShortBox", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } + "name": "Cornell Box", + "transforms": [ + { + "name": "camera", + "position": [ + 0.0, + 1.0, + 6.8 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ], + "cameras": [ + { + "name": "default", + "type": "perspective", + "fov": 19.5, + "transform": "camera" + } + ], + "materials": [ + { + "name": "LeftWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.63, + 0.065, + 0.05 + ] }, - { - "name": "TallBox", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] }, - { - "name": "Light", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [ 0.0, 0.0, 0.0] - } - } - ], - "objects": [ - { - "name": "mesh0", - "emission": { - "type": "rgb", - "unit": "radiance", - "value": [17.0, 12.0, 4.0] - }, - "material": "Light", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -0.24, - 1.98, - -0.22, - 0.23, - 1.98, - -0.22, - 0.23, - 1.98, - 0.16, - -0.24, - 1.98, - 0.16 - ], - "normals": [ - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh1", - "material": "Floor", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 1.74846e-07, - -1.0, - -1.0, - 1.74846e-07, - 1.0, - 1.0, - -1.74846e-07, - 1.0, - 1.0, - -1.74846e-07, - -1.0 - ], - "normals": [ - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0 + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "RightWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.14, + 0.45, + 0.091 ] }, - { - "name": "mesh2", - "material": "Ceiling", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - 1.0, - 2.0, - 1.0, - -1.0, - 2.0, - 1.0, - -1.0, - 2.0, - -1.0, - 1.0, - 2.0, - -1.0 - ], - "normals": [ - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0 + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 ] }, - { - "name": "mesh3", - "material": "BackWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 0.0, - -1.0, - -1.0, - 2.0, - -1.0, + "emission_color": { + "type": "rgb", + "value": [ 1.0, - 2.0, - -1.0, 1.0, - 0.0, - -1.0 - ], - "normals": [ - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh4", - "material": "RightWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - 1.0, - 0.0, - -1.0, - 1.0, - 2.0, - -1.0, - 1.0, - 2.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Floor", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 - ], - "normals": [ - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + ] + }, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Ceiling", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh5", - "material": "LeftWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 0.0, - 1.0, - -1.0, - 2.0, - 1.0, - -1.0, - 2.0, - -1.0, - -1.0, - 0.0, - -1.0 - ], - "normals": [ - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "BackWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh6", - "material": "ShortBox", - "type": "trimesh", - "indices": [ - 0, - 2, - 1, - 0, - 3, - 2, - 4, - 6, - 5, - 4, - 7, - 6, - 8, - 10, - 9, - 8, - 11, - 10, - 12, - 14, - 13, - 12, - 15, - 14, - 16, - 18, - 17, - 16, - 19, - 18, - 20, - 22, - 21, - 20, - 23, - 22 - ], - "vertices": [ - -0.0460751, - 0.6, - 0.573007, - -0.0460751, - -2.98023e-08, - 0.573007, - 0.124253, - 0.0, - 0.00310463, - 0.124253, - 0.6, - 0.00310463, - 0.533009, - 0.0, - 0.746079, - 0.533009, - 0.6, - 0.746079, - 0.703337, - 0.6, - 0.176177, - 0.703337, - 2.98023e-08, - 0.176177, - 0.533009, - 0.6, - 0.746079, - -0.0460751, - 0.6, - 0.573007, - 0.124253, - 0.6, - 0.00310463, - 0.703337, - 0.6, - 0.176177, - 0.703337, - 2.98023e-08, - 0.176177, - 0.124253, - 0.0, - 0.00310463, - -0.0460751, - -2.98023e-08, - 0.573007, - 0.533009, - 0.0, - 0.746079, - 0.533009, - 0.0, - 0.746079, - -0.0460751, - -2.98023e-08, - 0.573007, - -0.0460751, - 0.6, - 0.573007, - 0.533009, - 0.6, - 0.746079, - 0.703337, - 0.6, - 0.176177, - 0.124253, - 0.6, - 0.00310463, - 0.124253, - 0.0, - 0.00310463, - 0.703337, - 2.98023e-08, - 0.176177 - ], - "normals": [ - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "ShortBox", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + } + }, + { + "name": "TallBox", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh7", - "material": "TallBox", - "type": "trimesh", - "indices": [ - 0, - 2, - 1, - 0, - 3, - 2, - 4, - 6, - 5, - 4, - 7, - 6, - 8, - 10, - 9, - 8, - 11, - 10, - 12, - 14, - 13, - 12, - 15, - 14, - 16, - 18, - 17, - 16, - 19, - 18, - 20, - 22, - 21, - 20, - 23, - 22 - ], - "vertices": [ - -0.720444, - 1.2, - -0.473882, - -0.720444, - 0.0, - -0.473882, - -0.146892, - 0.0, - -0.673479, - -0.146892, - 1.2, - -0.673479, - -0.523986, - 0.0, - 0.0906493, - -0.523986, - 1.2, - 0.0906492, - 0.0495656, - 1.2, - -0.108948, - 0.0495656, - 0.0, - -0.108948, - -0.523986, - 1.2, - 0.0906492, - -0.720444, - 1.2, - -0.473882, - -0.146892, - 1.2, - -0.673479, - 0.0495656, - 1.2, - -0.108948, - 0.0495656, - 0.0, - -0.108948, - -0.146892, - 0.0, - -0.673479, - -0.720444, - 0.0, - -0.473882, - -0.523986, - 0.0, - 0.0906493, - -0.523986, - 0.0, - 0.0906493, - -0.720444, - 0.0, - -0.473882, - -0.720444, - 1.2, - -0.473882, - -0.523986, - 1.2, - 0.0906492, - 0.0495656, - 1.2, - -0.108948, - -0.146892, - 1.2, - -0.673479, - -0.146892, - 0.0, - -0.673479, - 0.0495656, - 0.0, - -0.108948 - ], - "normals": [ - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Light", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] - } - ] - } \ No newline at end of file + }, + "emission_strength": 10.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + } + ], + "objects": [ + { + "name": "mesh0", + "emission": { + "type": "rgb", + "unit": "radiance", + "value": [ + 17.0, + 12.0, + 4.0 + ] + }, + "material": "Light", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -0.24, + 1.98, + -0.22, + 0.23, + 1.98, + -0.22, + 0.23, + 1.98, + 0.16, + -0.24, + 1.98, + 0.16 + ], + "normals": [ + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh1", + "material": "Floor", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 1.74846e-07, + -1.0, + -1.0, + 1.74846e-07, + 1.0, + 1.0, + -1.74846e-07, + 1.0, + 1.0, + -1.74846e-07, + -1.0 + ], + "normals": [ + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh2", + "material": "Ceiling", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + 1.0, + 2.0, + 1.0, + -1.0, + 2.0, + 1.0, + -1.0, + 2.0, + -1.0, + 1.0, + 2.0, + -1.0 + ], + "normals": [ + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh3", + "material": "BackWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 0.0, + -1.0, + -1.0, + 2.0, + -1.0, + 1.0, + 2.0, + -1.0, + 1.0, + 0.0, + -1.0 + ], + "normals": [ + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh4", + "material": "RightWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + 1.0, + 0.0, + -1.0, + 1.0, + 2.0, + -1.0, + 1.0, + 2.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "normals": [ + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh5", + "material": "LeftWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 0.0, + 1.0, + -1.0, + 2.0, + 1.0, + -1.0, + 2.0, + -1.0, + -1.0, + 0.0, + -1.0 + ], + "normals": [ + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh6", + "material": "ShortBox", + "type": "trimesh", + "indices": [ + 0, + 2, + 1, + 0, + 3, + 2, + 4, + 6, + 5, + 4, + 7, + 6, + 8, + 10, + 9, + 8, + 11, + 10, + 12, + 14, + 13, + 12, + 15, + 14, + 16, + 18, + 17, + 16, + 19, + 18, + 20, + 22, + 21, + 20, + 23, + 22 + ], + "vertices": [ + -0.0460751, + 0.6, + 0.573007, + -0.0460751, + -2.98023e-08, + 0.573007, + 0.124253, + 0.0, + 0.00310463, + 0.124253, + 0.6, + 0.00310463, + 0.533009, + 0.0, + 0.746079, + 0.533009, + 0.6, + 0.746079, + 0.703337, + 0.6, + 0.176177, + 0.703337, + 2.98023e-08, + 0.176177, + 0.533009, + 0.6, + 0.746079, + -0.0460751, + 0.6, + 0.573007, + 0.124253, + 0.6, + 0.00310463, + 0.703337, + 0.6, + 0.176177, + 0.703337, + 2.98023e-08, + 0.176177, + 0.124253, + 0.0, + 0.00310463, + -0.0460751, + -2.98023e-08, + 0.573007, + 0.533009, + 0.0, + 0.746079, + 0.533009, + 0.0, + 0.746079, + -0.0460751, + -2.98023e-08, + 0.573007, + -0.0460751, + 0.6, + 0.573007, + 0.533009, + 0.6, + 0.746079, + 0.703337, + 0.6, + 0.176177, + 0.124253, + 0.6, + 0.00310463, + 0.124253, + 0.0, + 0.00310463, + 0.703337, + 2.98023e-08, + 0.176177 + ], + "normals": [ + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh7", + "material": "TallBox", + "type": "trimesh", + "indices": [ + 0, + 2, + 1, + 0, + 3, + 2, + 4, + 6, + 5, + 4, + 7, + 6, + 8, + 10, + 9, + 8, + 11, + 10, + 12, + 14, + 13, + 12, + 15, + 14, + 16, + 18, + 17, + 16, + 19, + 18, + 20, + 22, + 21, + 20, + 23, + 22 + ], + "vertices": [ + -0.720444, + 1.2, + -0.473882, + -0.720444, + 0.0, + -0.473882, + -0.146892, + 0.0, + -0.673479, + -0.146892, + 1.2, + -0.673479, + -0.523986, + 0.0, + 0.0906493, + -0.523986, + 1.2, + 0.0906492, + 0.0495656, + 1.2, + -0.108948, + 0.0495656, + 0.0, + -0.108948, + -0.523986, + 1.2, + 0.0906492, + -0.720444, + 1.2, + -0.473882, + -0.146892, + 1.2, + -0.673479, + 0.0495656, + 1.2, + -0.108948, + 0.0495656, + 0.0, + -0.108948, + -0.146892, + 0.0, + -0.673479, + -0.720444, + 0.0, + -0.473882, + -0.523986, + 0.0, + 0.0906493, + -0.523986, + 0.0, + 0.0906493, + -0.720444, + 0.0, + -0.473882, + -0.720444, + 1.2, + -0.473882, + -0.523986, + 1.2, + 0.0906492, + 0.0495656, + 1.2, + -0.108948, + -0.146892, + 1.2, + -0.673479, + -0.146892, + 0.0, + -0.673479, + 0.0495656, + 0.0, + -0.108948 + ], + "normals": [ + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + } + ] +} \ No newline at end of file From f9c75ebab1dea3bba02e4b3b89041034cc4a24eb Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Thu, 15 Jan 2026 10:05:51 +0100 Subject: [PATCH 06/13] reorganize the examples --- .../BlenderSync}/App.razor | 0 .../BlenderSync/BlenderSync.csproj | 0 .../BlenderSync}/Imports.cs | 0 .../BlenderSync}/MainLayout.razor | 0 .../BlenderSync}/Pages/BlenderImporter.razor | 0 .../Pages/BlenderImporter.razor.css | 0 .../BlenderSync}/Pages/CursorTracker.razor | 0 .../BlenderSync}/Pages/Experiment.razor | 0 .../BlenderSync}/Pages/Experiment.razor.cs | 3 +- .../BlenderSync}/Pages/Index.razor | 0 .../BlenderSync}/Pages/PathViewer.razor | 0 .../BlenderSync}/Pages/PathViewer.razor.cs | 0 .../BlenderSync}/Pages/_Host.cshtml | 2 +- .../BlenderSync}/Program.cs | 2 +- .../BlenderSync}/_Imports.razor | 2 +- .../BlenderSync}/appsettings.Development.json | 0 .../BlenderSync}/appsettings.json | 0 .../BlenderSync}/wwwroot/css/site.css | 0 .../BlenderSync}/wwwroot/raycast.js | 0 .../MisCompensation.dib | 0 .../SphericalSampling.dib | 0 .../Data/CornellBox/Meshes/Cube.0.0.ply | Bin 1184 -> 0 bytes .../Data/CornellBox/Meshes/Plane.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Plane.001.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Plane.002.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Plane.003.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Plane.004.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Plane.005.0.0.ply | Bin 458 -> 0 bytes .../Data/CornellBox/Meshes/Sphere.0.0.ply | Bin 72254 -> 0 bytes .../Example/Data/CornellBox/cornellbox.json | 490 ------------------ .../Data/ExportTest/Meshes/Cube.0.0.ply | Bin 1184 -> 0 bytes .../Data/ExportTest/Meshes/Plane.0.0.ply | Bin 458 -> 0 bytes .../Data/ExportTest/Meshes/Sphere.0.0.ply | Bin 72254 -> 0 bytes .../Example/Data/ExportTest/exporttest.json | 275 ---------- SeeSharp.Examples/Imports.cs | 30 -- SeeSharp.Examples/MakeFigure.py | 48 -- SeeSharp.Examples/PathVsVcm.cs | 16 - SeeSharp.Examples/Program.cs | 27 - SeeSharp.Examples/SeeSharp.Examples.csproj | 13 - SeeSharp.sln | 33 +- 40 files changed, 23 insertions(+), 918 deletions(-) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/App.razor (100%) rename SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj => Examples/BlenderSync/BlenderSync.csproj (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Imports.cs (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/MainLayout.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/BlenderImporter.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/BlenderImporter.razor.css (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/CursorTracker.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/Experiment.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/Experiment.razor.cs (96%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/Index.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/PathViewer.razor (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/PathViewer.razor.cs (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Pages/_Host.cshtml (96%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/Program.cs (95%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/_Imports.razor (81%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/appsettings.Development.json (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/appsettings.json (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/wwwroot/css/site.css (100%) rename {SeeSharp.Blender/Example => Examples/BlenderSync}/wwwroot/raycast.js (100%) rename {SeeSharp.Examples => Examples}/MisCompensation.dib (100%) rename {SeeSharp.Examples => Examples}/SphericalSampling.dib (100%) delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.001.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/Meshes/Sphere.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/CornellBox/cornellbox.json delete mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Cube.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Plane.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/ExportTest/Meshes/Sphere.0.0.ply delete mode 100644 SeeSharp.Blender/Example/Data/ExportTest/exporttest.json delete mode 100644 SeeSharp.Examples/Imports.cs delete mode 100644 SeeSharp.Examples/MakeFigure.py delete mode 100644 SeeSharp.Examples/PathVsVcm.cs delete mode 100644 SeeSharp.Examples/Program.cs delete mode 100644 SeeSharp.Examples/SeeSharp.Examples.csproj diff --git a/SeeSharp.Blender/Example/App.razor b/Examples/BlenderSync/App.razor similarity index 100% rename from SeeSharp.Blender/Example/App.razor rename to Examples/BlenderSync/App.razor diff --git a/SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj b/Examples/BlenderSync/BlenderSync.csproj similarity index 100% rename from SeeSharp.Blender/Example/SeeSharp.Blender.Example.csproj rename to Examples/BlenderSync/BlenderSync.csproj diff --git a/SeeSharp.Blender/Example/Imports.cs b/Examples/BlenderSync/Imports.cs similarity index 100% rename from SeeSharp.Blender/Example/Imports.cs rename to Examples/BlenderSync/Imports.cs diff --git a/SeeSharp.Blender/Example/MainLayout.razor b/Examples/BlenderSync/MainLayout.razor similarity index 100% rename from SeeSharp.Blender/Example/MainLayout.razor rename to Examples/BlenderSync/MainLayout.razor diff --git a/SeeSharp.Blender/Example/Pages/BlenderImporter.razor b/Examples/BlenderSync/Pages/BlenderImporter.razor similarity index 100% rename from SeeSharp.Blender/Example/Pages/BlenderImporter.razor rename to Examples/BlenderSync/Pages/BlenderImporter.razor diff --git a/SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css b/Examples/BlenderSync/Pages/BlenderImporter.razor.css similarity index 100% rename from SeeSharp.Blender/Example/Pages/BlenderImporter.razor.css rename to Examples/BlenderSync/Pages/BlenderImporter.razor.css diff --git a/SeeSharp.Blender/Example/Pages/CursorTracker.razor b/Examples/BlenderSync/Pages/CursorTracker.razor similarity index 100% rename from SeeSharp.Blender/Example/Pages/CursorTracker.razor rename to Examples/BlenderSync/Pages/CursorTracker.razor diff --git a/SeeSharp.Blender/Example/Pages/Experiment.razor b/Examples/BlenderSync/Pages/Experiment.razor similarity index 100% rename from SeeSharp.Blender/Example/Pages/Experiment.razor rename to Examples/BlenderSync/Pages/Experiment.razor diff --git a/SeeSharp.Blender/Example/Pages/Experiment.razor.cs b/Examples/BlenderSync/Pages/Experiment.razor.cs similarity index 96% rename from SeeSharp.Blender/Example/Pages/Experiment.razor.cs rename to Examples/BlenderSync/Pages/Experiment.razor.cs index 4cecd73c..2ee7003f 100644 --- a/SeeSharp.Blender/Example/Pages/Experiment.razor.cs +++ b/Examples/BlenderSync/Pages/Experiment.razor.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Components; -using SeeSharp.Blazor; -namespace SeeSharp.Blender.Example.Pages; +namespace BlenderSync.Pages; public partial class Experiment : ComponentBase { diff --git a/SeeSharp.Blender/Example/Pages/Index.razor b/Examples/BlenderSync/Pages/Index.razor similarity index 100% rename from SeeSharp.Blender/Example/Pages/Index.razor rename to Examples/BlenderSync/Pages/Index.razor diff --git a/SeeSharp.Blender/Example/Pages/PathViewer.razor b/Examples/BlenderSync/Pages/PathViewer.razor similarity index 100% rename from SeeSharp.Blender/Example/Pages/PathViewer.razor rename to Examples/BlenderSync/Pages/PathViewer.razor diff --git a/SeeSharp.Blender/Example/Pages/PathViewer.razor.cs b/Examples/BlenderSync/Pages/PathViewer.razor.cs similarity index 100% rename from SeeSharp.Blender/Example/Pages/PathViewer.razor.cs rename to Examples/BlenderSync/Pages/PathViewer.razor.cs diff --git a/SeeSharp.Blender/Example/Pages/_Host.cshtml b/Examples/BlenderSync/Pages/_Host.cshtml similarity index 96% rename from SeeSharp.Blender/Example/Pages/_Host.cshtml rename to Examples/BlenderSync/Pages/_Host.cshtml index db74ad21..cd8b74d8 100644 --- a/SeeSharp.Blender/Example/Pages/_Host.cshtml +++ b/Examples/BlenderSync/Pages/_Host.cshtml @@ -1,6 +1,6 @@ @page "/" @using Microsoft.AspNetCore.Components.Web -@namespace SeeSharp.Blender.Example.Pages +@namespace BlenderSync.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/SeeSharp.Blender/Example/Program.cs b/Examples/BlenderSync/Program.cs similarity index 95% rename from SeeSharp.Blender/Example/Program.cs rename to Examples/BlenderSync/Program.cs index 492d502f..7cb53f2c 100644 --- a/SeeSharp.Blender/Example/Program.cs +++ b/Examples/BlenderSync/Program.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components.Web; ProgressBar.Silent = true; -SceneRegistry.AddSourceRelativeToScript("./Data"); +SceneRegistry.AddSourceRelativeToScript("../../Data/Scenes"); var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); diff --git a/SeeSharp.Blender/Example/_Imports.razor b/Examples/BlenderSync/_Imports.razor similarity index 81% rename from SeeSharp.Blender/Example/_Imports.razor rename to Examples/BlenderSync/_Imports.razor index b5601de5..7d8a1710 100644 --- a/SeeSharp.Blender/Example/_Imports.razor +++ b/Examples/BlenderSync/_Imports.razor @@ -1,6 +1,6 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop -@using SeeSharp.Blender.Example +@using BlenderSync @using SeeSharp.Blazor diff --git a/SeeSharp.Blender/Example/appsettings.Development.json b/Examples/BlenderSync/appsettings.Development.json similarity index 100% rename from SeeSharp.Blender/Example/appsettings.Development.json rename to Examples/BlenderSync/appsettings.Development.json diff --git a/SeeSharp.Blender/Example/appsettings.json b/Examples/BlenderSync/appsettings.json similarity index 100% rename from SeeSharp.Blender/Example/appsettings.json rename to Examples/BlenderSync/appsettings.json diff --git a/SeeSharp.Blender/Example/wwwroot/css/site.css b/Examples/BlenderSync/wwwroot/css/site.css similarity index 100% rename from SeeSharp.Blender/Example/wwwroot/css/site.css rename to Examples/BlenderSync/wwwroot/css/site.css diff --git a/SeeSharp.Blender/Example/wwwroot/raycast.js b/Examples/BlenderSync/wwwroot/raycast.js similarity index 100% rename from SeeSharp.Blender/Example/wwwroot/raycast.js rename to Examples/BlenderSync/wwwroot/raycast.js diff --git a/SeeSharp.Examples/MisCompensation.dib b/Examples/MisCompensation.dib similarity index 100% rename from SeeSharp.Examples/MisCompensation.dib rename to Examples/MisCompensation.dib diff --git a/SeeSharp.Examples/SphericalSampling.dib b/Examples/SphericalSampling.dib similarity index 100% rename from SeeSharp.Examples/SphericalSampling.dib rename to Examples/SphericalSampling.dib diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Cube.0.0.ply deleted file mode 100644 index 56d35861fbd4180c541321e2cae35d4c43d1cf08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1184 zcmZXSzfTik7{?DN1y1o7D(gEq7>pO>s*~kTg*aebFeWA@^;+J_CD*%hJuC-NJ7SbX z7vsX;-+O}a&m#e$$^EzzW~3_`?k>cuF3ap`h5EQ`o3?~i}i94R-A|xUEc}g zg6Bq&#|zw_a~+@EFvj&#P^oY~V)sJsM0}1FV|LdA=ON1(H;o*7@MxA@XZ3pBD9X4I zgwJ*Eagp|dhY??56FI#a237osS=kHFa48vxlY!-Az)$7)sU$y@RZEtMj)yBdCC+Z0 zaL=tpY_T-&gly5p&T{yI3pYx`unf%C-JM(cM_cHOaAr!%lsAacT)#Hw#>Ed zu_v$a-<`=0@8qjo=6Cl`^M*xy6ZP+7K9y5_m7}kP3x|I4ZL~GslTRm z`$PG}S4BSY#w79I=%0mu>G~?~`Sr93e_Q>>XX?xzhPrhUSiMLY+`S$*TlmGNSh-1Gt7yB#s3x5(^z29_wTfDE1KOfMy zX|9>%lk6}3q5ky#R4)DPe!pq1`loW~Uj`;>Jy0*y2W2SI4`rbN3Jl^Hf^_H<1y17_ ahR#4|DR2(QdFTQ(LV=4oEVEnOt=b) z@(Y0MN`9y<)FVEnOt=b) z@(Y0MN`9yX#38|+y? NhA;v#6A&{4F#xWhss#W5 diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.002.0.0.ply deleted file mode 100644 index f3f69766a7bfc004a704c6ecaa49d6ee71df25cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmZXQzfQw25XSj8S&TfuZHR%0s_C4Hij55xrpQSzsU^pD9fve5bdf6jQi^?;W5JCpG%QDDB>BnPzV}%-FqnLGh)mc4IR#q)8y=P|Dk!gdmNVs z%Vz#{`~1F2pC(st!>!*nTHQBulQso$v%%}2y)q=Oi65OU$D96^*TDznzf%5TkGJkR N*lV|KsvW9bs&6d0s$l>C diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.003.0.0.ply deleted file mode 100644 index 9b9c8bac5adae44324dc6f89bb62b567ba60a990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmXTOspLw_FUn0UQAoVEnOt=b) z@(Y0MN`9yU)*!RaK;&j%P;yI=5KkpEBs3Op75Xb z_6$&P|NNcl?#3eDVBf_b?Eo@ACZ``w9%Mm-Jqw6p L1Y#y2W(HyaL-eU% diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.004.0.0.ply deleted file mode 100644 index 0939e11bfa28bc5459abb324a9abfd5c78fbb6bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmXTOspLw_FUn0UQAoVEnOt=b) z@(Y0MN`9ytT@*__P^`*J4fC# zW`Kh2?ZpmdmYW^4G2|QUK}vQ3l9Qx}VQ1yf48|*EC=Jx>2=faQ&>29!R0a1)V K%ml>DKnwtd>!OkX diff --git a/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply b/SeeSharp.Blender/Example/Data/CornellBox/Meshes/Plane.005.0.0.ply deleted file mode 100644 index 773daf8173bbde627a2e33b0138d6880a666a451..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 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

h^G&*B5Vif}!@D8$G7^ZMrBk?Hwn^PGFa^N-yA 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 -/// Renders a scene with a path tracer and with VCM. -/// -class PathVsVcm : Experiment { - public override List MakeMethods() => [ - new("PathTracer", new PathTracer() { TotalSpp = 4 }), - new("Vcm", new VertexConnectionAndMerging() { NumIterations = 2 }) - ]; -} \ No newline at end of file diff --git a/SeeSharp.Examples/Program.cs b/SeeSharp.Examples/Program.cs deleted file mode 100644 index 928e49db..00000000 --- a/SeeSharp.Examples/Program.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics; -using SeeSharp.Experiments; -using SeeSharp.Images; -using SeeSharp.Examples; - -// Register the directory as a scene file provider. -// Asides from the geometry, it is also used as a reference image cache. -SceneRegistry.AddSource("Data/Scenes"); - -// Configure a benchmark to compare path tracing and VCM on the CornellBox -// at 512x512 resolution. Display images in tev during rendering (localhost, default port) -Benchmark benchmark = new(new PathVsVcm(), [ - SceneRegistry.LoadScene("CornellBox", maxDepth: 5), - // SceneRegistry.LoadScene("CornellBox", maxDepth: 2).WithName("CornellBoxDirectIllum") -], "Results/PathVsVcm", 512, 512, FrameBuffer.Flags.SendToTev); - -// Render the images -benchmark.Run(); - -// Optional, but usually a good idea: assemble the rendering results in an overview -// figure using a Python script. -Process.Start("python", "./SeeSharp.Examples/MakeFigure.py Results/PathVsVcm PathTracer Vcm") - .WaitForExit(); - -// For our README file, we further convert the pdf to png with ImageMagick -Process.Start("magick", "-density 300 ./Results/PathVsVcm/Overview.pdf ExampleFigure.png") - .WaitForExit(); diff --git a/SeeSharp.Examples/SeeSharp.Examples.csproj b/SeeSharp.Examples/SeeSharp.Examples.csproj deleted file mode 100644 index 1bebeb4c..00000000 --- a/SeeSharp.Examples/SeeSharp.Examples.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - Exe - net9.0 - false - - - diff --git a/SeeSharp.sln b/SeeSharp.sln index 68ee0080..b1687443 100644 --- a/SeeSharp.sln +++ b/SeeSharp.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.IntegrationTests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Benchmark", "SeeSharp.Benchmark\SeeSharp.Benchmark.csproj", "{957383F5-B5A0-4CA5-A283-0F2154FFC96A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Examples", "SeeSharp.Examples\SeeSharp.Examples.csproj", "{5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.PreviewRender", "SeeSharp.PreviewRender\SeeSharp.PreviewRender.csproj", "{93EBE289-351C-4F29-95CA-B559F3675031}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.ToMitsuba", "SeeSharp.ToMitsuba\SeeSharp.ToMitsuba.csproj", "{83FE3967-6070-4BF3-8861-6410B877CB45}" @@ -27,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest", "MaterialTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blender", "SeeSharp.Blender\SeeSharp.Blender.csproj", "{BB86935A-DF0E-4B98-9823-41A15F85BD7D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlenderSync", "Examples\BlenderSync\BlenderSync.csproj", "{B7B4BA23-171F-49F2-B711-654C3D8F2A18}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,18 +99,6 @@ Global {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x64.Build.0 = Release|Any CPU {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x86.ActiveCfg = Release|Any CPU {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x86.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x64.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x64.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x86.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x86.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|Any CPU.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x64.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x64.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x86.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x86.Build.0 = Release|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|Any CPU.Build.0 = Debug|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -181,8 +171,23 @@ Global {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x64.Build.0 = Release|Any CPU {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.ActiveCfg = Release|Any CPU {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x64.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x86.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x64.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x64.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x86.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B7B4BA23-171F-49F2-B711-654C3D8F2A18} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + EndGlobalSection EndGlobal From 32de8011285f99ae9d9151be37caaabab8857867 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Thu, 15 Jan 2026 11:09:37 +0100 Subject: [PATCH 07/13] fix links --- Examples/BlenderSync/Pages/Index.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BlenderSync/Pages/Index.razor b/Examples/BlenderSync/Pages/Index.razor index 9bbea8ef..dcc10eb2 100644 --- a/Examples/BlenderSync/Pages/Index.razor +++ b/Examples/BlenderSync/Pages/Index.razor @@ -31,7 +31,7 @@ foreach (var routableComponent in routableComponents) { - string name = routableComponent.ToString().Replace("SeeSharp.Blender.Example.Pages.", string.Empty); + string name = routableComponent.ToString().Replace("BlenderSync.Pages.", string.Empty); if (name != "Index") yield return (name, name); } From 84a99e991db979f547e6c2d157160eba3eb434e2 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Sun, 18 Jan 2026 23:26:18 +0100 Subject: [PATCH 08/13] support obj, fix material --- BlenderExtension/see_blender/importer.py | 65 +++++++++++++++++------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/BlenderExtension/see_blender/importer.py b/BlenderExtension/see_blender/importer.py index 34a2081e..a1147aae 100644 --- a/BlenderExtension/see_blender/importer.py +++ b/BlenderExtension/see_blender/importer.py @@ -37,16 +37,18 @@ def make_material(name, mat_json, base_path): links.new(principled.outputs["BSDF"], output.inputs["Surface"]) # -------- Base color (texture or rgb) - base_color = mat_json["baseColor"] - if base_color["type"] == "rgb": - principled.inputs["Base Color"].default_value = base_color["value"] + [1.0] - elif base_color["type"] == "image": - img_path = os.path.join(base_path, base_color["filename"]) - img = load_image(img_path) - if img: - tex = nodes.new("ShaderNodeTexImage") - tex.image = img - links.new(tex.outputs["Color"], principled.inputs["Base Color"]) + # base_color = mat_json["baseColor"] + base_color = mat_json.get("baseColor") + if base_color: + if base_color["type"] == "rgb": + principled.inputs["Base Color"].default_value = base_color["value"] + [1.0] + elif base_color["type"] == "image": + img_path = os.path.join(base_path, base_color["filename"]) + img = load_image(img_path) + if img: + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + links.new(tex.outputs["Color"], principled.inputs["Base Color"]) # -------- Roughness (texture or float) roughness = mat_json.get("roughness", 1.0) @@ -69,26 +71,51 @@ def make_material(name, mat_json, base_path): # Emission emission_json = mat_json.get("emission") if emission_json and emission_json.get("type") == "rgb": - color = mat_json["emission_color"]["value"] + # color = mat_json["emission_color"]["value"] + if "emission_color" in mat_json: + color = mat_json["emission_color"].get("value", [1.0, 1.0, 1.0]) + else: + # fallback to emission value itself + color = emission_json.get("value", [0.0, 0.0, 0.0]) + strength = mat_json.get("emission_strength", 0.0) principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) - principled.inputs["Emission Strength"].default_value = mat_json["emission_strength"] - if mat_json.get("emissionIsGlossy", False): - principled.inputs["Emission Strength"].default_value = mat_json["emissionExponent"] + principled.inputs["Emission Strength"].default_value = strength + # if mat_json.get("emissionIsGlossy", False): + # principled.inputs["Emission Strength"].default_value = mat_json["emissionExponent"] return mat +def load_mesh(filepath): + ext = os.path.splitext(filepath)[1].lower() -def load_ply(filepath): - """Load a .ply mesh and return the created object.""" before = set(bpy.data.objects) - bpy.ops.wm.ply_import(filepath=filepath) - after = set(bpy.data.objects) + if ext == ".ply": + bpy.ops.wm.ply_import(filepath=filepath) + + elif ext == ".obj": + bpy.ops.wm.obj_import(filepath=filepath) + + else: + raise RuntimeError(f"Unsupported mesh format: {ext}") + + after = set(bpy.data.objects) new_objs = list(after - before) if new_objs: return new_objs[0] return None +# def load_ply(filepath): +# """Load a .ply mesh and return the created object.""" +# before = set(bpy.data.objects) +# bpy.ops.wm.ply_import(filepath=filepath) +# after = set(bpy.data.objects) + +# new_objs = list(after - before) +# if new_objs: +# return new_objs[0] +# return None + def import_trimesh_object(obj, mat_lookup): name = obj.get("name", "Trimesh") @@ -254,7 +281,7 @@ def import_seesharp(filepath): else: # fallback to existing PLY logic ply_path = os.path.join(base_path, obj["relativePath"]) - new_obj = load_ply(ply_path) + new_obj = load_mesh(ply_path) if not new_obj: print(f"Failed to load {ply_path}") continue From 1a70efa4821d05599148d4bfafaf761e0ea4de09 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Sun, 18 Jan 2026 23:59:48 +0100 Subject: [PATCH 09/13] fix the merge in Import.cs --- Examples/BlenderSync/Imports.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/BlenderSync/Imports.cs b/Examples/BlenderSync/Imports.cs index b6c33e5a..dee65517 100644 --- a/Examples/BlenderSync/Imports.cs +++ b/Examples/BlenderSync/Imports.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS8019 + global using System; global using System.Collections.Concurrent; global using System.Collections.Generic; From 7ed6ee7b8c9d29ce6e0fc6ba8d14c71feea8c428 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 20 Jan 2026 01:10:33 +0100 Subject: [PATCH 10/13] add json serialization support for PathGraphNode --- .../path_viewer/commands/create_path.py | 44 +++- .../BlenderSync/Pages/BlenderImporter.razor | 2 +- Examples/BlenderSync/Pages/PathViewer.razor | 12 +- .../BlenderSync/Pages/PathViewer.razor.cs | 214 ------------------ SeeSharp/Integrators/Util/PathGraph.cs | 24 +- 5 files changed, 65 insertions(+), 231 deletions(-) delete mode 100644 Examples/BlenderSync/Pages/PathViewer.razor.cs diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py index a4f8902f..06a7fba8 100644 --- a/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py @@ -7,6 +7,36 @@ def handle_create_path(msg): json_string = msg.get("graph") user_group_id = msg.get("id") data = json.loads(json_string) + + def flatten_tree(root): + nodes: list[dict] = [] + pairs: list[tuple[str, str]] = [] + visited = set() + + def visit(node: dict): + node_id = node["Id"] + if node_id in visited: + return + visited.add(node_id) + + # collect node + + flat_node = { + k: v for k, v in node.items() + if k != "Successors" + } + nodes.append(flat_node) + # collect parent-child edge (skip roots) + parent_id = node.get("ancestorId") + if parent_id is not None: + pairs.append((parent_id, node_id)) + + # recurse + for child in node.get("Successors", []): + visit(child) + + visit(root) + return nodes, pairs def create_fixed_arrow(A, B, properties, name="Arrow"): A = Vector(A) B = Vector(B) @@ -115,13 +145,15 @@ def run(): bpy.context.scene.collection.children.link(col) id_to_node = {} - for node in data["nodes"]: - pos = node["position"] - id_to_node[node["id"]] = {"pos": renderer_to_blender_world(Vector((pos["X"], pos["Y"], pos["Z"]))), - "data": node["data"], - "type": node["type"]} + nodes, pairs = flatten_tree(data) + + for node in nodes: + pos = node["Position"] + id_to_node[node["Id"]] = {"pos": renderer_to_blender_world(Vector((pos["X"], pos["Y"], pos["Z"]))), + "data": node, + "type": node["$type"]} - for start_id, end_id in data["edges"]: + for start_id, end_id in pairs: if (id_to_node[start_id]["type"] == "LightPathNode" or id_to_node[end_id]["type"] == "LightPathNode"): # reverse the direction if its light path temp_id = start_id diff --git a/Examples/BlenderSync/Pages/BlenderImporter.razor b/Examples/BlenderSync/Pages/BlenderImporter.razor index 53dc5880..622c1d1b 100644 --- a/Examples/BlenderSync/Pages/BlenderImporter.razor +++ b/Examples/BlenderSync/Pages/BlenderImporter.razor @@ -105,7 +105,7 @@ public string FindJson(string folder) { - folder = "Data/" + folder; + folder = "../../Data/Scenes/" + folder; folder = Path.GetFullPath(folder); var files = Directory.GetFiles(folder, "*.json"); if (files.Length == 0) diff --git a/Examples/BlenderSync/Pages/PathViewer.razor b/Examples/BlenderSync/Pages/PathViewer.razor index 92a8f5d8..ac32a78a 100644 --- a/Examples/BlenderSync/Pages/PathViewer.razor +++ b/Examples/BlenderSync/Pages/PathViewer.razor @@ -156,13 +156,17 @@ (graph, estimate) = integrator.ReplayPixel(scene, new Pixel((int)pt.x, (int)pt.y), 0); PathGraphNode node = graph.Roots[0]; viewer_root = graph.Roots[0]; - string jsonGraph = GraphSerializer.SerializeGraph(node); + string jsonGraph = System.Text.Json.JsonSerializer.Serialize(node, new JsonSerializerOptions() + { + IncludeFields = true, + }); + Console.WriteLine(jsonGraph); string path_id = Guid.NewGuid().ToString(); viewer_roots.Add(path_id, graph.Roots[0]); await Commander.SendCommandAsync(new { command = "create_path", - id = path_id, + id = path_id, graph = jsonGraph }); } @@ -183,7 +187,7 @@ var node_list = new List(); for (var n = node; n != null; n = n.Ancestor) { - node_list.Add(n.GetHashCode().ToString("X")); + node_list.Add(n.Id); } node_list.Reverse(); @@ -206,7 +210,7 @@ var node_list = new List(); for (var n = node; n != null; n = n.Ancestor) { - node_list.Add(n.GetHashCode().ToString("X")); + node_list.Add(n.Id); } node_list.Reverse(); diff --git a/Examples/BlenderSync/Pages/PathViewer.razor.cs b/Examples/BlenderSync/Pages/PathViewer.razor.cs deleted file mode 100644 index b56ed6f8..00000000 --- a/Examples/BlenderSync/Pages/PathViewer.razor.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Text.Json; - -public record Vec3DTO(float X, float Y, float Z); -public record Vec2DTO(float X, float Y); - -public record SurfacePointDTO(Vec3DTO Position, Vec3DTO Normal, Vec2DTO BarycentricCoords, - Mesh Mesh, uint PrimId, float ErrorOffset, - float Distance, Vec3DTO ShadingNormal, Vec2DTO TextureCoordinates, Material Material); - -public record PathVertexDTO(SurfacePointDTO Point, float PdfFromAncestor, float PdfReverseAncestor, - float PdfNextEventAncestor, Vec3DTO DirToAncestor, float JacobianToAncestor, - RgbColor Weight, int PathId, byte Depth, float MaximumRoughness, bool FromBackground); -public class NodeDTO -{ - public string NodeType { get; set; } - public Vec3DTO Position { get; set; } - public Dictionary Data { get; set; } = new(); -} - -public static class GraphNodeSerializer -{ - public static NodeDTO Serialize(PathGraphNode node) - { - return node switch - { - NextEventNode n => SerializeNextEventNode(n), - BSDFSampleNode n => SerializeBSDFSampleNode(n), - LightPathNode n => SerializeLightPathNode(n), - ConnectionNode n => SerializeConnectionNode(n), - MergeNode n => SerializeMergeNode(n), - BackgroundNode n => SerializeBackgroundNode(n), - _ => SerializeBase(node) - }; - } - - public static Vec3DTO ToDTO(this Vector3 v) - => new(v.X, v.Y, v.Z); - - public static Vec2DTO ToDTO(this Vector2 v) - => new(v.X, v.Y); - public static SurfacePointDTO toDTO(this SurfacePoint s) - => new(s.Position.ToDTO(), s.Normal.ToDTO(), s.BarycentricCoords.ToDTO(), - s.Mesh, s.PrimId, s.ErrorOffset, s.Distance, s.ShadingNormal.ToDTO(), s.TextureCoordinates.ToDTO(), s.Material); - - public static PathVertexDTO toDTO(this PathVertex p) - => new(p.Point.toDTO(), p.PdfFromAncestor, p.PdfReverseAncestor, p.PdfNextEventAncestor, p.DirToAncestor.ToDTO(), - p.JacobianToAncestor, p.Weight, p.PathId, p.Depth, p.MaximumRoughness, p.FromBackground); - private static NodeDTO SerializeBase(PathGraphNode n) - { - return new NodeDTO - { - NodeType = n.GetType().Name, - Position = n.Position.ToDTO(), - Data = { - ["IsBackground"] = n.IsBackground, - ["SuccessorCount"] = n.Successors.Count - } - }; - } - - private static NodeDTO SerializeNextEventNode(NextEventNode n) - { - return new NodeDTO - { - NodeType = "NextEventNode", - Position = n.Position.ToDTO(), - Data = { - ["Emission"] = n.Emission, - ["Pdf"] = n.Pdf, - ["BsdfTimesCosine"] = n.BsdfTimesCosine, - ["MISWeight"] = n.MISWeight, - ["PrefixWeight"] = n.PrefixWeight, - ["HasSurfacePoint"] = n.Point != null - } - }; - } - - private static NodeDTO SerializeBSDFSampleNode(BSDFSampleNode n) - { - return new NodeDTO - { - NodeType = "BSDFSampleNode", - Position = n.Position.ToDTO(), - Data = { - ["ScatterWeight"] = n.ScatterWeight, - ["SurvivalProbability"] = n.SurvivalProbability, - ["Emission"] = n.Emission, - ["MISWeight"] = n.MISWeight, - ["SurfacePoint"] = n.Point.toDTO() - } - }; - } - - private static NodeDTO SerializeLightPathNode(LightPathNode n) - { - return new NodeDTO - { - NodeType = "LightPathNode", - Position = n.Position.ToDTO(), - Data = { - ["LightVertex"] = n.LightVertex.toDTO() - } - }; - } - - private static NodeDTO SerializeConnectionNode(ConnectionNode n) - { - return new NodeDTO - { - NodeType = "ConnectionNode", - Position = n.Position.ToDTO(), - Data = { - ["Contrib"] = n.Contrib, - ["MISWeight"] = n.MISWeight, - ["LightVertex"] = n.LightVertex.toDTO() - } - }; - } - - private static NodeDTO SerializeMergeNode(MergeNode n) - { - return new NodeDTO - { - NodeType = "MergeNode", - Position = n.Position.ToDTO(), - Data = { - ["Contrib"] = n.Contrib, - ["MISWeight"] = n.MISWeight, - ["LightVertex"] = n.LightVertex.toDTO() - } - }; - } - - private static NodeDTO SerializeBackgroundNode(BackgroundNode n) - { - return new NodeDTO - { - NodeType = "BackgroundNode", - Position = n.Position.ToDTO(), - Data = { - ["Emission"] = n.Emission, - ["MISWeight"] = n.MISWeight - } - }; - } -} - -public static class NodeIdGenerator -{ - public static string GetId(PathGraphNode node) - => node.GetHashCode().ToString("X"); // stable enough for session -} - -public static class GraphSerializer -{ - public static string SerializeGraph(PathGraphNode root) - { - var visited = new HashSet(); - var nodes = new List>(); - var edges = new List<(string, string)>(); - - Traverse(root, visited, nodes, edges); - - var final = new - { - nodes, - edges = edges.Select(e => new[] { e.Item1, e.Item2 }) - }; - - return JsonSerializer.Serialize(final, new JsonSerializerOptions - { - WriteIndented = false - }); - } - - private static void Traverse( - PathGraphNode node, - HashSet visited, - List> nodes, - List<(string, string)> edges) - { - if (node == null || visited.Contains(node)) - return; - - visited.Add(node); - - string id = NodeIdGenerator.GetId(node); - var dto = GraphNodeSerializer.Serialize(node); - - // Convert DTO into raw dictionary for JSON - var nodeDict = new Dictionary - { - ["id"] = id, - ["type"] = dto.NodeType, - ["position"] = dto.Position, // Vec3DTO (serializable) - ["data"] = dto.Data // Dictionary - }; - - nodes.Add(nodeDict); - - // Traverse successors - foreach (var child in node.Successors) - { - if (child != null) - { - string childId = NodeIdGenerator.GetId(child); - edges.Add((id, childId)); - } - - Traverse(child, visited, nodes, edges); - } - } -} - diff --git a/SeeSharp/Integrators/Util/PathGraph.cs b/SeeSharp/Integrators/Util/PathGraph.cs index dce389af..92723b00 100644 --- a/SeeSharp/Integrators/Util/PathGraph.cs +++ b/SeeSharp/Integrators/Util/PathGraph.cs @@ -2,9 +2,21 @@ namespace SeeSharp.Integrators.Util; +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(PathGraphNode), "PathGraphNode")] +[JsonDerivedType(typeof(NextEventNode), "NextEventNode")] +[JsonDerivedType(typeof(BSDFSampleNode), "BSDFSampleNode")] +[JsonDerivedType(typeof(LightPathNode), "LightPathNode")] +[JsonDerivedType(typeof(ConnectionNode), "ConnectionNode")] +[JsonDerivedType(typeof(MergeNode), "MergeNode")] +[JsonDerivedType(typeof(BackgroundNode), "BackgroundNode")] public class PathGraphNode(Vector3 pos, PathGraphNode ancestor = null) { - public Vector3 Position = pos; - public PathGraphNode Ancestor = ancestor; + public string Id { get; init; } = Guid.NewGuid().ToString("N")[..30]; + public Vector3 Position { get; set; } = pos; + [JsonIgnore] public PathGraphNode Ancestor = ancestor; + + [JsonPropertyName("ancestorId")] + public string? AncestorId => Ancestor?.Id; public List Successors = []; public virtual bool IsBackground => false; @@ -92,13 +104,13 @@ public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survival public readonly float SurvivalProbability; public readonly RgbColor Emission; public readonly float MISWeight; - public readonly SurfacePoint Point; + public SurfacePoint Point { get; } public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(41, 107, 177); } public class LightPathNode(PathVertex lightVertex) : PathGraphNode(lightVertex.Point.Position) { - public readonly PathVertex LightVertex = lightVertex; + public PathVertex LightVertex { get; } = lightVertex; public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(228, 135, 17); } @@ -113,7 +125,7 @@ public ConnectionNode(PathVertex lightVertex, float misWeight, RgbColor contrib) public RgbColor Contrib { get; init; } public float MISWeight { get; init; } - public readonly PathVertex LightVertex; + public PathVertex LightVertex { get; } public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(167, 214, 170); } @@ -128,7 +140,7 @@ public MergeNode(PathVertex lightVertex, float misWeight, RgbColor contrib) public RgbColor Contrib { get; init; } public float MISWeight { get; init; } - public readonly PathVertex LightVertex; + public PathVertex LightVertex { get; } public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(218, 152, 204); } From 409d5fc7f7325cf48d24ee12b698f0118dca67b3 Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 20 Jan 2026 14:32:03 +0100 Subject: [PATCH 11/13] refactor PathGraph --- SeeSharp/Integrators/Util/PathGraph.cs | 156 ------------------ .../Util/PathGraph/BSDFSampleNode.cs | 25 +++ .../Util/PathGraph/BackgroundNode.cs | 13 ++ .../Util/PathGraph/ConnectionNode.cs | 16 ++ .../Util/PathGraph/LightPathNode.cs | 7 + .../Integrators/Util/PathGraph/MergeNode.cs | 16 ++ .../Util/PathGraph/NextEventNode.cs | 37 +++++ .../Util/PathGraph/PathGraphNode.cs | 47 ++++++ 8 files changed, 161 insertions(+), 156 deletions(-) create mode 100644 SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/MergeNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs create mode 100644 SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs diff --git a/SeeSharp/Integrators/Util/PathGraph.cs b/SeeSharp/Integrators/Util/PathGraph.cs index 92723b00..824b4d10 100644 --- a/SeeSharp/Integrators/Util/PathGraph.cs +++ b/SeeSharp/Integrators/Util/PathGraph.cs @@ -1,162 +1,6 @@ using System.Linq; namespace SeeSharp.Integrators.Util; - -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(PathGraphNode), "PathGraphNode")] -[JsonDerivedType(typeof(NextEventNode), "NextEventNode")] -[JsonDerivedType(typeof(BSDFSampleNode), "BSDFSampleNode")] -[JsonDerivedType(typeof(LightPathNode), "LightPathNode")] -[JsonDerivedType(typeof(ConnectionNode), "ConnectionNode")] -[JsonDerivedType(typeof(MergeNode), "MergeNode")] -[JsonDerivedType(typeof(BackgroundNode), "BackgroundNode")] -public class PathGraphNode(Vector3 pos, PathGraphNode ancestor = null) { - public string Id { get; init; } = Guid.NewGuid().ToString("N")[..30]; - public Vector3 Position { get; set; } = pos; - [JsonIgnore] public PathGraphNode Ancestor = ancestor; - - [JsonPropertyName("ancestorId")] - public string? AncestorId => Ancestor?.Id; - public List Successors = []; - - public virtual bool IsBackground => false; - - public PathGraphNode AddSuccessor(PathGraphNode vertex) { - Successors.Add(vertex); - vertex.Ancestor = this; - return vertex; - } - - public virtual RgbColor ComputeVisualizerColor() { - return RgbColor.Black; - } - - public PathGraphNode Clone() { - var result = MemberwiseClone() as PathGraphNode; - result.Successors = []; - foreach (var s in Successors) { - var sClone = s.Clone(); - sClone.Ancestor = result; - result.Successors.Add(sClone); - } - return result; - } -} - -public interface IContribNode { - public RgbColor Contrib { get; } - public float MISWeight { get; } -} - -public class NextEventNode : PathGraphNode, IContribNode { - public NextEventNode(Vector3 direction, PathGraphNode ancestor, RgbColor emission, float pdf, - RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) - : base(ancestor.Position + direction, ancestor) { - Emission = emission; - Pdf = pdf; - BsdfTimesCosine = bsdfCos; - MISWeight = misWeight; - PrefixWeight = prefixWeight; - } - - public NextEventNode(SurfacePoint point, RgbColor emission, float pdf, RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) - : base(point.Position) { - Emission = emission; - Pdf = pdf; - BsdfTimesCosine = bsdfCos; - MISWeight = misWeight; - Point = point; - PrefixWeight = prefixWeight; - } - - public readonly RgbColor Emission; - public readonly float Pdf; - public readonly RgbColor BsdfTimesCosine; - public readonly float MISWeight; - public readonly SurfacePoint? Point; - public readonly RgbColor PrefixWeight; - - public override bool IsBackground => !Point.HasValue; - - public RgbColor Contrib => PrefixWeight * Emission / Pdf * BsdfTimesCosine; - - float IContribNode.MISWeight => MISWeight; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); -} - -public class BSDFSampleNode : PathGraphNode { - public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb) : base(point.Position) { - ScatterWeight = scatterWeight; - SurvivalProbability = survivalProb; - Point = point; - } - - public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb, RgbColor emission, float misWeight) : base(point.Position) { - ScatterWeight = scatterWeight; - SurvivalProbability = survivalProb; - Emission = emission; - MISWeight = misWeight; - Point = point; - } - - public readonly RgbColor ScatterWeight; - public readonly float SurvivalProbability; - public readonly RgbColor Emission; - public readonly float MISWeight; - public SurfacePoint Point { get; } - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(41, 107, 177); -} - -public class LightPathNode(PathVertex lightVertex) : PathGraphNode(lightVertex.Point.Position) { - public PathVertex LightVertex { get; } = lightVertex; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(228, 135, 17); -} - -public class ConnectionNode : PathGraphNode, IContribNode { - public ConnectionNode(PathVertex lightVertex, float misWeight, RgbColor contrib) - : base(lightVertex.Point.Position) { - Contrib = contrib; - MISWeight = misWeight; - LightVertex = lightVertex; - } - - public RgbColor Contrib { get; init; } - public float MISWeight { get; init; } - public PathVertex LightVertex { get; } - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(167, 214, 170); -} - -public class MergeNode : PathGraphNode, IContribNode { - public MergeNode(PathVertex lightVertex, float misWeight, RgbColor contrib) - : base(lightVertex.Point.Position) { - Contrib = contrib; - MISWeight = misWeight; - LightVertex = lightVertex; - } - - public RgbColor Contrib { get; init; } - public float MISWeight { get; init; } - public PathVertex LightVertex { get; } - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(218, 152, 204); -} - -public class BackgroundNode : PathGraphNode { - public BackgroundNode(Vector3 direction, PathGraphNode ancestor, RgbColor contrib, float misWeight) : base(ancestor.Position + direction) { - Emission = contrib; - MISWeight = misWeight; - } - public readonly RgbColor Emission; - public readonly float MISWeight; - public override bool IsBackground => true; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); -} - public class PathGraph { public List Roots = []; diff --git a/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs b/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs new file mode 100644 index 00000000..f4e219c9 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs @@ -0,0 +1,25 @@ +namespace SeeSharp.Integrators.Util; + +public class BSDFSampleNode : PathGraphNode { + public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb) : base(point.Position) { + ScatterWeight = scatterWeight; + SurvivalProbability = survivalProb; + Point = point; + } + + public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb, RgbColor emission, float misWeight) : base(point.Position) { + ScatterWeight = scatterWeight; + SurvivalProbability = survivalProb; + Emission = emission; + MISWeight = misWeight; + Point = point; + } + + public readonly RgbColor ScatterWeight; + public readonly float SurvivalProbability; + public readonly RgbColor Emission; + public readonly float MISWeight; + public SurfacePoint Point { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(41, 107, 177); +} diff --git a/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs b/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs new file mode 100644 index 00000000..a8a1bdfe --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs @@ -0,0 +1,13 @@ +namespace SeeSharp.Integrators.Util; + +public class BackgroundNode : PathGraphNode { + public BackgroundNode(Vector3 direction, PathGraphNode ancestor, RgbColor contrib, float misWeight) : base(ancestor.Position + direction) { + Emission = contrib; + MISWeight = misWeight; + } + public readonly RgbColor Emission; + public readonly float MISWeight; + public override bool IsBackground => true; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs b/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs new file mode 100644 index 00000000..5e1de10a --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs @@ -0,0 +1,16 @@ +namespace SeeSharp.Integrators.Util; + +public class ConnectionNode : PathGraphNode, IContribNode { + public ConnectionNode(PathVertex lightVertex, float misWeight, RgbColor contrib) + : base(lightVertex.Point.Position) { + Contrib = contrib; + MISWeight = misWeight; + LightVertex = lightVertex; + } + + public RgbColor Contrib { get; init; } + public float MISWeight { get; init; } + public PathVertex LightVertex { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(167, 214, 170); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs b/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs new file mode 100644 index 00000000..8cb19022 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs @@ -0,0 +1,7 @@ +namespace SeeSharp.Integrators.Util; + +public class LightPathNode(PathVertex lightVertex) : PathGraphNode(lightVertex.Point.Position) { + public PathVertex LightVertex { get; } = lightVertex; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(228, 135, 17); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs b/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs new file mode 100644 index 00000000..cf12e04a --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs @@ -0,0 +1,16 @@ +namespace SeeSharp.Integrators.Util; + +public class MergeNode : PathGraphNode, IContribNode { + public MergeNode(PathVertex lightVertex, float misWeight, RgbColor contrib) + : base(lightVertex.Point.Position) { + Contrib = contrib; + MISWeight = misWeight; + LightVertex = lightVertex; + } + + public RgbColor Contrib { get; init; } + public float MISWeight { get; init; } + public PathVertex LightVertex { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(218, 152, 204); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs b/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs new file mode 100644 index 00000000..aa7dace3 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs @@ -0,0 +1,37 @@ +namespace SeeSharp.Integrators.Util; +public class NextEventNode : PathGraphNode, IContribNode { + public NextEventNode(Vector3 direction, PathGraphNode ancestor, RgbColor emission, float pdf, + RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) + : base(ancestor.Position + direction, ancestor) { + Emission = emission; + Pdf = pdf; + BsdfTimesCosine = bsdfCos; + MISWeight = misWeight; + PrefixWeight = prefixWeight; + } + + public NextEventNode(SurfacePoint point, RgbColor emission, float pdf, RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) + : base(point.Position) { + Emission = emission; + Pdf = pdf; + BsdfTimesCosine = bsdfCos; + MISWeight = misWeight; + Point = point; + PrefixWeight = prefixWeight; + } + + public readonly RgbColor Emission; + public readonly float Pdf; + public readonly RgbColor BsdfTimesCosine; + public readonly float MISWeight; + public readonly SurfacePoint? Point; + public readonly RgbColor PrefixWeight; + + public override bool IsBackground => !Point.HasValue; + + public RgbColor Contrib => PrefixWeight * Emission / Pdf * BsdfTimesCosine; + + float IContribNode.MISWeight => MISWeight; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); +} diff --git a/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs b/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs new file mode 100644 index 00000000..53ccfb2f --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs @@ -0,0 +1,47 @@ +namespace SeeSharp.Integrators.Util; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(PathGraphNode), "PathGraphNode")] +[JsonDerivedType(typeof(NextEventNode), "NextEventNode")] +[JsonDerivedType(typeof(BSDFSampleNode), "BSDFSampleNode")] +[JsonDerivedType(typeof(LightPathNode), "LightPathNode")] +[JsonDerivedType(typeof(ConnectionNode), "ConnectionNode")] +[JsonDerivedType(typeof(MergeNode), "MergeNode")] +[JsonDerivedType(typeof(BackgroundNode), "BackgroundNode")] +public class PathGraphNode(Vector3 pos, PathGraphNode ancestor = null) { + public string Id { get; init; } = Guid.NewGuid().ToString("N")[..30]; + public Vector3 Position { get; set; } = pos; + [JsonIgnore] public PathGraphNode Ancestor = ancestor; + + [JsonPropertyName("ancestorId")] + public string? AncestorId => Ancestor?.Id; + public List Successors = []; + + public virtual bool IsBackground => false; + + public PathGraphNode AddSuccessor(PathGraphNode vertex) { + Successors.Add(vertex); + vertex.Ancestor = this; + return vertex; + } + + public virtual RgbColor ComputeVisualizerColor() { + return RgbColor.Black; + } + + public PathGraphNode Clone() { + var result = MemberwiseClone() as PathGraphNode; + result.Successors = []; + foreach (var s in Successors) { + var sClone = s.Clone(); + sClone.Ancestor = result; + result.Successors.Add(sClone); + } + return result; + } +} + +public interface IContribNode { + public RgbColor Contrib { get; } + public float MISWeight { get; } +} \ No newline at end of file From b6e78fb6b4b99dc87999c93b9762b354104c62b1 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Mon, 26 Jan 2026 17:41:01 +0100 Subject: [PATCH 12/13] fix css loading issue --- .gitignore | 2 -- .../BlenderSync/Pages/BlenderImporter.razor | 3 +- Examples/BlenderSync/Pages/_Host.cshtml | 2 +- .../Properties/launchSettings.json | 35 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 Examples/BlenderSync/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index e657b0a2..0315f7dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # User specific editor and IDE configurations .vs -launchSettings.json -!SeeSharp.Templates/content/SeeSharp.Blazor.Template/Properties/launchSettings.json .idea SeeSharp.sln.DotSettings.user diff --git a/Examples/BlenderSync/Pages/BlenderImporter.razor b/Examples/BlenderSync/Pages/BlenderImporter.razor index 622c1d1b..829b7cbd 100644 --- a/Examples/BlenderSync/Pages/BlenderImporter.razor +++ b/Examples/BlenderSync/Pages/BlenderImporter.razor @@ -5,7 +5,6 @@ @using System; @using System.IO; -@namespace SeeSharp.Blender

- + @Html.Raw(SeeSharp.Blazor.Scripts.AllScripts) diff --git a/Examples/BlenderSync/Properties/launchSettings.json b/Examples/BlenderSync/Properties/launchSettings.json new file mode 100644 index 00000000..a99c6ef0 --- /dev/null +++ b/Examples/BlenderSync/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "iisExpress": { + "applicationUrl": "http://localhost:18831", + "sslPort": 44326 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7055;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} From 6808f42d46ca06ff4d5ee13f4f63eae9eb18412f Mon Sep 17 00:00:00 2001 From: NhatMinh2208 Date: Tue, 27 Jan 2026 14:35:11 +0100 Subject: [PATCH 13/13] importer: support obj, transformation matrix, baked emission --- BlenderExtension/see_blender/importer.py | 188 +++++++++++++++-------- 1 file changed, 122 insertions(+), 66 deletions(-) diff --git a/BlenderExtension/see_blender/importer.py b/BlenderExtension/see_blender/importer.py index a1147aae..9f6205d1 100644 --- a/BlenderExtension/see_blender/importer.py +++ b/BlenderExtension/see_blender/importer.py @@ -74,48 +74,58 @@ def make_material(name, mat_json, base_path): # color = mat_json["emission_color"]["value"] if "emission_color" in mat_json: color = mat_json["emission_color"].get("value", [1.0, 1.0, 1.0]) + strength = mat_json.get("emission_strength", 0.0) + principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) + principled.inputs["Emission Strength"].default_value = strength else: # fallback to emission value itself - color = emission_json.get("value", [0.0, 0.0, 0.0]) - strength = mat_json.get("emission_strength", 0.0) - principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) - principled.inputs["Emission Strength"].default_value = strength + raw = emission_json.get("value", [0.0, 0.0, 0.0]) + strength = max(raw) + if strength > 0.0: + color = [c / strength for c in raw] + else: + color = [0.0, 0.0, 0.0] + principled.inputs["Emission Color"].default_value = (*color, 1.0) + principled.inputs["Emission Strength"].default_value = strength + # color = emission_json.get("value", [0.0, 0.0, 0.0]) + # principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) + # principled.inputs["Emission Strength"].default_value = color[0] + # strength = mat_json.get("emission_strength", 0.0) + # principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) + # principled.inputs["Emission Strength"].default_value = strength # if mat_json.get("emissionIsGlossy", False): # principled.inputs["Emission Strength"].default_value = mat_json["emissionExponent"] return mat -def load_mesh(filepath): - ext = os.path.splitext(filepath)[1].lower() - +def load_mesh_with_transform(filepath, global_matrix): + """ + Load a .ply or .obj mesh and apply a transformation to all imported objects. + Returns a list of imported objects. + """ before = set(bpy.data.objects) + bpy.ops.wm.obj_import(filepath=filepath) + + after = set(bpy.data.objects) + new_objs = list(after - before) - if ext == ".ply": - bpy.ops.wm.ply_import(filepath=filepath) - - elif ext == ".obj": - bpy.ops.wm.obj_import(filepath=filepath) + # Apply the global transform to all imported objects + for obj in new_objs: + obj.matrix_world = global_matrix - else: - raise RuntimeError(f"Unsupported mesh format: {ext}") + return new_objs +def load_ply(filepath): + """Load a .ply mesh and return the created object.""" + before = set(bpy.data.objects) + bpy.ops.wm.ply_import(filepath=filepath) after = set(bpy.data.objects) + new_objs = list(after - before) if new_objs: return new_objs[0] return None -# def load_ply(filepath): -# """Load a .ply mesh and return the created object.""" -# before = set(bpy.data.objects) -# bpy.ops.wm.ply_import(filepath=filepath) -# after = set(bpy.data.objects) - -# new_objs = list(after - before) -# if new_objs: -# return new_objs[0] -# return None - def import_trimesh_object(obj, mat_lookup): name = obj.get("name", "Trimesh") @@ -191,18 +201,31 @@ def import_camera(cam_json, transform_json, scene): scene.camera = cam_obj # ------------ Transform - pos = transform_json["position"] - rot = transform_json["rotation"] + if "matrix" in transform_json: + m = transform_json["matrix"] + + # JSON is row-major → Blender needs column-major + mat = mathutils.Matrix(( + (m[0], m[4], m[8], m[12]), + (m[1], m[5], m[9], m[13]), + (m[2], m[6], m[10], m[14]), + (m[3], m[7], m[11], m[15]), + )) + conv = axis_conversion(from_forward="Z", from_up="Y").to_4x4() + cam_obj.matrix_world = conv @ mat + else: + pos = transform_json.get("position", [0, 0, 0]) + rot = transform_json.get("rotation", [0, 0, 0]) - cam_obj.location = (-pos[0], pos[2], pos[1]) + cam_obj.location = (-pos[0], pos[2], pos[1]) - # inverse Euler mapping - eul = mathutils.Euler(( - math.radians(rot[0] + 90), # x_euler - math.radians(rot[2]), # y_euler - math.radians(rot[1] - 180) # z_euler - ), 'XYZ') - cam_obj.rotation_euler = eul + # inverse Euler mapping + eul = mathutils.Euler(( + math.radians(rot[0] + 90), # x_euler + math.radians(rot[2]), # y_euler + math.radians(rot[1] - 180) # z_euler + ), 'XYZ') + cam_obj.rotation_euler = eul # ------------ FOV (vertical → horizontal) vert_fov = math.radians(cam_json["fov"]) @@ -274,43 +297,76 @@ def import_seesharp(filepath): # ------------------------------------------------------------------ global_matrix = axis_conversion(from_forward="Z", from_up="Y").to_4x4() + # APPLY OBJECT-LEVEL EMISSION FOR OLDER EXPORTER VERSION + def apply_object_emission(obj, emission_json): + if not emission_json: + return + + mat = obj.data.materials[0] + mat.use_nodes = True + nt = mat.node_tree + principled = next( + n for n in nt.nodes + if n.type == "BSDF_PRINCIPLED" + ) + + color = emission_json.get("value", [0.0, 0.0, 0.0]) + + # SeeSharp uses radiance → treat magnitude as strength + strength = max(color) + + principled.inputs["Emission Color"].default_value = ( + color[0] / strength, + color[1] / strength, + color[2] / strength, + 1.0 + ) + principled.inputs["Emission Strength"].default_value = strength + + if "objects" in data: for obj in data["objects"]: if obj.get("type") == "trimesh": new_obj = import_trimesh_object(obj, mat_lookup) + new_obj.matrix_world = global_matrix + + # APPLY OBJECT-LEVEL EMISSION + if "emission" in obj: + apply_object_emission(new_obj, obj["emission"]) else: # fallback to existing PLY logic - ply_path = os.path.join(base_path, obj["relativePath"]) - new_obj = load_mesh(ply_path) - if not new_obj: - print(f"Failed to load {ply_path}") - continue - - if obj.get("material") in mat_lookup: - new_obj.data.materials.append(mat_lookup[obj["material"]]) - # Apply transform (shared) - new_obj.matrix_world = global_matrix.inverted() - # for obj in data["objects"]: - # # ply_path = os.path.join(base_path, obj["relativePath"]) - # rel_path = obj.get("relativePath") - - # if not rel_path: - # print(f"⚠ Skipping object '{obj.get('name', 'UNKNOWN')}', no relativePath") - # continue - - # ply_path = os.path.join(base_path, rel_path) - # new_obj = load_ply(ply_path) - # if not new_obj: - # print(f"Failed to load {ply_path}") - # continue - - # # Apply SeeSharp → Blender inverse transform - # new_obj.matrix_world = global_matrix.inverted() - - # # Assign material - # if obj.get("material") in mat_lookup: - # new_obj.data.materials.append(mat_lookup[obj["material"]]) - + path = os.path.join(base_path, obj["relativePath"]) + ext = os.path.splitext(path)[1].lower() + + if ext == ".ply": + new_obj = load_ply(path) + if not new_obj: + print(f"Failed to load {path}") + continue + if obj.get("material") in mat_lookup: + new_obj.data.materials.append(mat_lookup[obj["material"]]) + new_obj.matrix_world = global_matrix + + # APPLY OBJECT-LEVEL EMISSION + if "emission" in obj: + apply_object_emission(new_obj, obj["emission"]) + elif ext == ".obj": + new_objs = load_mesh_with_transform(path, global_matrix) + if not new_objs: + print(f"Failed to load {path}") + continue + # Assign material to all loaded objects + for new_obj in new_objs: + mat_name = new_obj.get("material") + if mat_name in mat_lookup: + new_obj.data.materials.append(mat_lookup[mat_name]) + + # APPLY OBJECT-LEVEL EMISSION + if "emission" in obj: + apply_object_emission(new_obj, obj["emission"]) + else: + raise RuntimeError(f"Unsupported mesh format: {ext}") + bpy.context.scene.render.engine = "SEE_SHARP" try: