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/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..a1147aae
--- /dev/null
+++ b/BlenderExtension/see_blender/importer.py
@@ -0,0 +1,369 @@
+import os
+import json
+import math
+import bpy
+import mathutils
+import bmesh
+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"]
+ 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)
+ 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.get("emission")
+ if emission_json and emission_json.get("type") == "rgb":
+ # 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 = 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()
+
+ before = 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")
+
+ 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
+# ------------------------------------------------------------------------
+
+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"]:
+ 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_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"]])
+
+ 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..06a7fba8
--- /dev/null
+++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py
@@ -0,0 +1,190 @@
+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 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)
+ 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 = {}
+ 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 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
+ 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/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
diff --git a/Examples/BlenderSync/App.razor b/Examples/BlenderSync/App.razor
new file mode 100644
index 00000000..6fd3ed1b
--- /dev/null
+++ b/Examples/BlenderSync/App.razor
@@ -0,0 +1,12 @@
+ Sorry, there's nothing at this address.
Loading...
+ } + else if (!string.IsNullOrEmpty(scene?.Name)) + { +Loaded "@(scene.Name)"
+ } + +Waiting for Blender...
+} +else +{ +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
+ } +| 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) |
Rendering...
+ } +