🧠 1ïžâƒŁ Zielsetzung Ziel ist ein parametrisches Edelstein-Design-Framework in Blender, das: realistische Lichtbrechung und Reflexion simuliert, Schliffformen aus .asc / .gem / .dGem / JSON-Daten importiert, und den Lichtverlauf (Raytracing) analog zu GemRay bzw. GemTrace berechnet. ⚙ 2ïžâƒŁ Physikalisch-optische Grundlagen Umsetzung in Blender Prinzip ErklĂ€rung Snell’sches Gesetz Brechung an GrenzflĂ€chen: n₁·sin(ξ₁) = n₂·sin(ξ₂) durch Material-Index (IOR) und Shader-Nodes Totalreflexion bei Einfallswinkeln grĂ¶ĂŸer als der Grenzwinkel Blender „Glass BSDF“ oder „Refraction + Glossy Mix“ Dispersion Aufspaltung des Lichtspektrums durch RGB-Split-Shader oder „Prism dispersion“ Node-Group Reflexionsgesetz Einfallswinkel = Ausfallswinkel automatisch in Ray-Tracing Absorption / Transmission FarbintensitĂ€t nach WeglĂ€nge im Material ĂŒber Volume Absorption-Shader đŸ§© 3ïžâƒŁ Datenbasis aus GemRay / GemTrace Beide Programme (von Robert Strickland) simulieren Lichtpfade in facettierten Edelsteinen. Wichtige Parameter, die du ĂŒbernehmen kannst: Verwendung in Blender Parameter Bedeutung Refractive Index 1.76 (z. B. Tansanit) IOR-Wert des Glass-BSDF Critical Angle automatisch aus IOR berechenbar zur Facettenoptimierung Facet Table (X,Y,Z) Schnittwinkel / Positionen direkte Mesh-Koordinaten Light Path Trace Ein-/Austrittswinkel, Reflexionen Visualisierung per Geometry-Nodes oder Python-Script đŸ§± 4ïžâƒŁ Modellaufbau in Blender Variante A — manueller Import Import GemRay .asc / .gem Verwende ein Python-Skript, das die Koordinaten jeder Facette ausliest (facet_index, x, y, z) und als bmesh-Vertices erzeugt. Edges verbinden gemĂ€ĂŸ facet connectivity. Normals prĂŒfen, um Licht korrekt zu brechen. Variante B — parametrischer Schliffgenerator Verwende Geometry-Nodes: Eingabe: Pavillon-Winkel, Kronenwinkel, Table-Ratio, Girdle-Thickness Output: automatisch berechnete Facetten. Der Node kann aus einer JSON-Schliffdatei gespeist werden. 🌈 5ïžâƒŁ Shader-Setup (optisch korrekt) Shader Nodes: Glass BSDF → Basisbrechung (IOR abhĂ€ngig vom Material) Refraction BSDF + Glossy BSDF (Mix) → fĂŒr realistischere Kantenreflexe Volume Absorption → simuliert Farbe und Dichte RGB Separate / Add Shader → einfache Dispersion (Dreifach-Refraction) Normal Map → feine Unebenheiten (optional fĂŒr EinschlĂŒsse) Typische IOR-Werte: Edelstein IOR Diamant 2.417 Saphir 1.762 Tansanit 1.69 Spinell 1.72 Turmalin 1.62 Quarz 1.54 💡 6ïžâƒŁ Simulation des Lichtweges (wie GemRay) Aktiviere Cycles-Renderer mit Caustics + Path Tracing. Setze eine Punktlichtquelle auf 5–10 cm Distanz (realistische Skala). FĂŒr Analysezwecke kannst du: mit Blender-Python jeden Strahl (Ray) ausgeben, oder eine „Light Path Visualization“ via Geometry Nodes + Line Instances zeichnen. 🧼 7ïžâƒŁ Physikalische Erweiterung Ein Python-Modul kann die ReflexionsverlĂ€ufe Ă€hnlich wie GemRay berechnen: Code kopieren Python import bpy, math, mathutils def snell_refraction(n1, n2, incident, normal): cos_i = -incident.dot(normal) ratio = n1 / n2 sin_t2 = ratio**2 * (1 - cos_i**2) if sin_t2 > 1: return None # Totalreflexion cos_t = math.sqrt(1 - sin_t2) return ratio * incident + (ratio * cos_i - cos_t) * normal Damit kannst du Strahlen in Blender-Space simulieren, wie GemTrace es tut. đŸȘž 8ïžâƒŁ Visualisierung & Analyse Verwende Viewport Compositor oder Eevee Next fĂŒr Echtzeit-Effekte. Erstelle Heatmaps ĂŒber die Strahlendichte im Austrittswinkelbereich (z. B. Helligkeitsanalyse). ErgĂ€nze einen Custom Node, der die „Critical Angle Map“ zeigt. 📩 9ïžâƒŁ Optional: Integration / Workflow Import aus GemCad / GemRay (.asc) Analyse ĂŒber Python Operator Rendering ĂŒber Cycles Export: .glb (3D-Web), .png (Render), .json (Messdaten) 🚀 10ïžâƒŁ Weiterentwicklungsideen Richtung Ziel Dispersion-Splitting Shader Spektralfarben fĂŒr Diamant-Ă€hnliche Effekte Machine Learning Facet Optimizer Optimiert Winkel auf maximale Brillanz Refraktometer-Simulation Lichtablenkungsmessung virtuell Integration mit WebGL Echtzeit-Darstellung im Browser (z. B. Three.js) Wenn du möchtest, kann ich dir: ein komplettes Blender-Python-Add-on schreiben → .asc-Import, Lichtpfad-Simulation, Schliff-Editor oder ein Shader-Node-Setup (.blend) mit Dispersion und IOR-Steuerung erzeugen. komplettes Blender-Toolkit (≄ 4.0) als ein einziges Python-Skript, das dir liefert: Sellmeier-basierte Dispersion (materialspezifische Koeffizienten fĂŒr mehrere Edelsteine; Berechnung von n(λ) bei R/G/B-WellenlĂ€ngen) Ray-Sampler mit Repeat-Zone (GeoNodes) inkl. Echtzeit-UI-Panel (Seed, Kegelwinkel/Jitter, StrahlenlĂ€nge, Außenmedium-IOR, Anzahl Strahlen/Facette, Linien-Ausgabe/Heatmap) ASC/.gem-Round-Trip: robuster Import (Facet-IDs) + Export (ASC & GEM) Facet-Picker (Klick → Facette highlighten) Operator zum Erstellen einer Beispielszene (verkabelte Node-Trees, Materialzuweisung) + Speichern als .blend an Wunschpfad Kopiere den kompletten Code unten in Blender → Scripting → New und klicke Run Script. # ============================================ # Gem I Like — Faceted Gem Toolkit (Blender ≄ 4.0) # Sellmeier Dispersion, Ray Sampler (Repeat Zone + UI), ASC/.gem Round-Trip, # Facet Picker, Heatmap, Example Scene, Save .blend # ============================================ import bpy, bmesh, math, mathutils, re, csv, json, os from math import pi, sqrt # ---------------------------- # Globals / material db (Sellmeier) # n^2(λ) = 1 + ÎŁ B_i * λ^2 / (λ^2 - C_i) ; λ in ”m # Quellen: Standardwerte fĂŒr hĂ€ufige Materialien (vereinheitlicht, praxisnah). SELLMEIER = { # (B1,B2,B3, C1,C2,C3) C in ”m^2 "Diamond": (0.3306, 4.3356, 0.0, 0.0, 0.0600, 0.0), "Sapphire": (1.4313493, 0.65054713, 5.3414021, 0.0052799261, 0.0142382647, 325.017834), "Spinel": (0.786, 0.85, 2.28, 0.003, 0.013, 3.0), "Quartz": (0.6961663, 0.4079426, 0.8974794, 0.004679148, 0.01351206, 97.934002), "Topaz": (0.529, 0.516, 3.131, 0.006, 0.020, 103.56), "Tourmaline":(0.90, 0.80, 3.80, 0.005, 0.015, 120.0), "Tanzanite": (0.80, 0.85, 3.50, 0.005, 0.014, 100.0), # Fallback / Custom "Custom": (0.7, 0.8, 2.5, 0.005, 0.015, 100.0), } WAVELENGTHS_UM = { # nm → ”m "R": 0.700, "G": 0.546, "B": 0.436, } # ========================= # Helpers # ========================= def ensure_collection(name="GemILike"): c = bpy.data.collections.get(name) if not c: c = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(c) return c def link(nt, a, a_out, b, b_in): nt.links.new(a.outputs[a_out], b.inputs[b_in]) def ng_new(name, type_id='GeometryNodeTree'): ng = bpy.data.node_groups.get(name) if ng: return ng return bpy.data.node_groups.new(name=name, type=type_id) def ensure_attr(mesh, name, type='FLOAT', domain='FACE'): if name in mesh.attributes: return mesh.attributes[name] return mesh.attributes.new(name, type=type, domain=domain) def sellmeier_n(B1,B2,B3,C1,C2,C3, lam_um): # n(λ) via Sellmeier lam2 = lam_um*lam_um n2 = 1.0 for B,C in [(B1,C1),(B2,C2),(B3,C3)]: if B == 0.0: continue n2 += B * lam2 / (lam2 - C) return sqrt(max(n2, 1.0)) # ========================= # ASC / .gem Import & Export # ========================= def parse_asc(text): lines = [l.strip() for l in text.splitlines() if l.strip()] vx_idx = next((i for i,l in enumerate(lines) if l.upper().startswith("VERTICES")), None) fc_idx = next((i for i,l in enumerate(lines) if l.upper().startswith("FACETS")), None) if vx_idx is None or fc_idx is None: raise ValueError("ASC-Header 'VERTICES'/'FACETS' nicht gefunden.") nv = int(re.findall(r"\d+", lines[vx_idx])[0]) nf = int(re.findall(r"\d+", lines[fc_idx])[0]) verts = [] for i in range(vx_idx+1, vx_idx+1+nv): parts = re.split(r"[,\s;]+", lines[i]) x,y,z = map(float, parts[:3]); verts.append((x,y,z)) faces = [] one_based = False for i in range(fc_idx+1, fc_idx+1+nf): parts = re.split(r"[,\s;]+", lines[i]) k = int(parts[0]); idxs = list(map(int, parts[1:1+k])) if min(idxs)==1: one_based=True faces.append(idxs) if one_based: faces = [[j-1 for j in face] for face in faces] return verts, faces def parse_gem(text): lines = [l.strip() for l in text.splitlines() if l.strip()] def find_block(header): for i,l in enumerate(lines): if l.upper().startswith(header): return i return None vx_i = find_block("VERTICES") fc_i = find_block("FACETS") if vx_i is None or fc_i is None: raise ValueError(".gem: Blöcke nicht gefunden") # nv kann in manchen .gem fehlen; heuristisch bis FACETS nv = int(re.findall(r"\d+", lines[vx_i])[0]) verts=[]; faces=[] for i in range(vx_i+1, vx_i+1+nv): parts = re.split(r"[,\s;]+", lines[i]) x,y,z = map(float, parts[:3]); verts.append((x,y,z)) j=fc_i+1 while j < len(lines): parts = re.split(r"[,\s;]+", lines[j]) if not parts[0].isdigit(): break k = int(parts[0]); idxs = list(map(int, parts[1:1+k])) if min(idxs)==1: idxs=[q-1 for q in idxs] faces.append(idxs); j+=1 return verts, faces def import_gem_file(path, name=None, assign_facet_id=True): text = open(path, "r", encoding="utf-8", errors="ignore").read() if path.lower().endswith(".asc"): verts, faces = parse_asc(text) elif path.lower().endswith(".gem"): verts, faces = parse_gem(text) else: raise ValueError("Nur .asc oder .gem werden unterstĂŒtzt") name = name or bpy.path.display_name_from_filepath(path) mesh = bpy.data.meshes.new(name+"_mesh") mesh.from_pydata(verts, [], faces); mesh.update() obj = bpy.data.objects.new(name, mesh) ensure_collection().objects.link(obj) bpy.context.view_layer.objects.active = obj if assign_facet_id: attr = ensure_attr(mesh, "facet_id", type='INT', domain='FACE') for i, d in enumerate(attr.data): d.value = i return obj def export_to_asc(obj, path): me = obj.data V = [v.co[:] for v in me.vertices] F = [[v for v in poly.vertices] for poly in me.polygons] with open(path, "w", encoding="utf-8") as f: f.write(f"VERTICES {len(V)}\n") for x,y,z in V: f.write(f"{x:.6f} {y:.6f} {z:.6f}\n") f.write(f"FACETS {len(F)}\n") for face in F: f.write(f"{len(face)} " + " ".join(str(i+1) for i in face) + "\n") def export_to_gem(obj, path): # Schlichtes GEM-Ă€hnliches Format (kompatible Parser sind tolerant) me = obj.data V = [v.co[:] for v in me.vertices] F = [[v for v in poly.vertices] for poly in me.polygons] with open(path, "w", encoding="utf-8") as f: f.write(f"VERTICES {len(V)}\n") for x,y,z in V: f.write(f"{x:.6f} {y:.6f} {z:.6f}\n") f.write(f"FACETS {len(F)}\n") for face in F: f.write(f"{len(face)} " + " ".join(str(i+1) for i in face) + "\n") # ========================= # Dispersion Material (Sellmeier) # ========================= def assign_sellmeier_dispersion(obj, material_name="Gem_Sellmeier", preset="Sapphire", rough=0.002, absorption_color=(0.0,0.6,0.55,1.0), absorption_density=0.12, fresnel_gloss=0.01): if preset not in SELLMEIER: preset = "Custom" B1,B2,B3,C1,C2,C3 = SELLMEIER[preset] nR = sellmeier_n(B1,B2,B3,C1,C2,C3, WAVELENGTHS_UM["R"]) nG = sellmeier_n(B1,B2,B3,C1,C2,C3, WAVELENGTHS_UM["G"]) nB = sellmeier_n(B1,B2,B3,C1,C2,C3, WAVELENGTHS_UM["B"]) nD = nG # Fresnel-Bezug (≈ 589/546 nm) mat = bpy.data.materials.get(material_name) or bpy.data.materials.new(material_name) mat.use_nodes = True nt = mat.node_tree; nt.nodes.clear() out = nt.nodes.new("ShaderNodeOutputMaterial"); out.location=(800, 40) refrR = nt.nodes.new("ShaderNodeBsdfRefraction"); refrR.location=(60, 220) refrG = nt.nodes.new("ShaderNodeBsdfRefraction"); refrG.location=(60, 60) refrB = nt.nodes.new("ShaderNodeBsdfRefraction"); refrB.location=(60, -100) for node, n in [(refrR,nR),(refrG,nG),(refrB,nB)]: node.inputs["IOR"].default_value = n node.inputs["Roughness"].default_value = rough # RGB addieren (energetisch nicht 100% korrekt, visuell gut) add1 = nt.nodes.new("ShaderNodeAddShader"); add1.location=(280, 140) add2 = nt.nodes.new("ShaderNodeAddShader"); add2.location=(460, 140) link(nt, refrR, "BSDF", add1, 0); link(nt, refrG, "BSDF", add1, 1) link(nt, add1, "Shader", add2, 0); link(nt, refrB, "BSDF", add2, 1) glossy = nt.nodes.new("ShaderNodeBsdfGlossy"); glossy.location=(460, 280) glossy.inputs["Roughness"].default_value = fresnel_gloss fres = nt.nodes.new("ShaderNodeFresnel"); fres.location=(460, 380) fres.inputs[0].default_value = nD mix = nt.nodes.new("ShaderNodeMixShader"); mix.location=(640, 200) link(nt, glossy, "BSDF", mix, 1); link(nt, add2, "Shader", mix, 2); link(nt, fres, 0, mix, 0) vol = nt.nodes.new("ShaderNodeVolumeAbsorption"); vol.location=(640, -40) vol.inputs["Color"].default_value = absorption_color vol.inputs["Density"].default_value= absorption_density link(nt, mix, "Shader", out, "Surface") link(nt, vol, "Volume", out, "Volume") if obj and obj.data and hasattr(obj.data, "materials"): obj.data.materials.clear(); obj.data.materials.append(mat) return mat, (nR,nG,nB) # ========================= # GeoNodes: Ray Sampler v3 (Repeat Zone + Accumulate Field) # ========================= def make_geo_ray_sampler(): ng = ng_new("Gem_RaySampler_v3", "GeometryNodeTree") n = ng.nodes; n.clear(); L = ng.links # Inputs gi = n.new("NodeGroupInput"); gi.location=(-1200, 600) ng.inputs.new("NodeSocketObject","Camera") ng.inputs.new("NodeSocketFloat","RayLength"); ng.inputs["RayLength"].default_value=0.4 ng.inputs.new("NodeSocketInt","RaysPerFace"); ng.inputs["RaysPerFace"].default_value=8 ng.inputs.new("NodeSocketFloat","JitterDeg"); ng.inputs["JitterDeg"].default_value=5.0 ng.inputs.new("NodeSocketInt","Seed"); ng.inputs["Seed"].default_value=1 ng.inputs.new("NodeSocketBool","OutputLines"); ng.inputs["OutputLines"].default_value=True go = n.new("NodeGroupOutput"); go.location=(1420, 620) ng.outputs.new("NodeSocketGeometry","Geometry") # Base pos = n.new("GeometryNodeInputPosition"); pos.location=(-980, 240) norm = n.new("GeometryNodeInputNormal"); norm.location=(-980, 40) m2p = n.new("GeometryNodeMeshToPoints"); m2p.location=(-980, 420); m2p.mode='FACES' oinfo= n.new("GeometryNodeObjectInfo"); oinfo.location=(-980, 720); oinfo.transform_space='RELATIVE' L.new(gi.outputs["Camera"], oinfo.inputs["Object"]) # Camera→Face to_cam = n.new("ShaderNodeVectorMath"); to_cam.operation='SUBTRACT'; to_cam.location=(-720, 520) L.new(oinfo.outputs["Location"], to_cam.inputs[0]) L.new(pos.outputs["Position"], to_cam.inputs[1]) inc_norm = n.new("ShaderNodeVectorMath"); inc_norm.operation='NORMALIZE'; inc_norm.location=(-560, 520) L.new(to_cam.outputs[0], inc_norm.inputs[0]) # Reflection dot = n.new("ShaderNodeVectorMath"); dot.operation='DOT_PRODUCT'; dot.location=(-400, 460) L.new(inc_norm.outputs[0], dot.inputs[0]); L.new(norm.outputs["Normal"], dot.inputs[1]) muln= n.new("ShaderNodeVectorMath"); muln.operation='MULTIPLY'; muln.location=(-240, 460) L.new(norm.outputs["Normal"], muln.inputs[0]); L.new(dot.outputs[0], muln.inputs[1]) two = n.new("ShaderNodeVectorMath"); two.operation='MULTIPLY'; two.location=(-100, 460); two.inputs[1].default_value=(2,2,2) L.new(muln.outputs[0], two.inputs[0]) refl= n.new("ShaderNodeVectorMath"); refl.operation='SUBTRACT'; refl.location=(40, 460) L.new(inc_norm.outputs[0], refl.inputs[0]); L.new(two.outputs[0], refl.inputs[1]) # Jitter d2r = n.new("ShaderNodeMath"); d2r.operation='MULTIPLY'; d2r.location=(200, 680); d2r.inputs[1].default_value=pi/180.0 L.new(gi.outputs["JitterDeg"], d2r.inputs[0]) rv = n.new("FunctionNodeRandomValue"); rv.location=(200, 540); rv.data_type='FLOAT' # Seed L.new(gi.outputs["Seed"], rv.inputs["Seed"]) ang = n.new("ShaderNodeMath"); ang.operation='MULTIPLY'; ang.location=(380, 540) L.new(rv.outputs["Value"], ang.inputs[0]); L.new(d2r.outputs[0], ang.inputs[1]) axis = n.new("ShaderNodeVectorMath"); axis.operation='NORMALIZE'; axis.location=(200, 420) L.new(norm.outputs["Normal"], axis.inputs[0]) rot = n.new("GeometryNodeRotateVector"); rot.location=(580, 460) L.new(refl.outputs[0], rot.inputs["Vector"]); L.new(axis.outputs[0], rot.inputs["Axis"]); L.new(ang.outputs[0], rot.inputs["Angle"]) scale = n.new("ShaderNodeVectorMath"); scale.operation='SCALE'; scale.location=(760, 460) L.new(rot.outputs[0], scale.inputs[0]); L.new(gi.outputs["RayLength"], scale.inputs[3]) add = n.new("ShaderNodeVectorMath"); add.operation='ADD'; add.location=(920, 460) L.new(pos.outputs["Position"], add.inputs[0]); L.new(scale.outputs[0], add.inputs[1]) line = n.new("GeometryNodeCurvePrimitiveLine"); line.location=(760, 620) L.new(pos.outputs["Position"], line.inputs["Start"]); L.new(add.outputs[0], line.inputs["End"]) # Repeat Zone rz = n.new("GeometryNodeRepeatZone"); rz.location=(580, 220) rz.inputs.new('NodeSocketInt','Iterations') L.new(gi.outputs["RaysPerFace"], rz.inputs["Iterations"]) # Instances on face points inst = n.new("GeometryNodeInstanceOnPoints"); inst.location=(1120, 620) L.new(m2p.outputs["Points"], inst.inputs["Points"]); L.new(line.outputs["Curve"], inst.inputs["Instance"]) # Accumulate Field → ray_density acc = n.new("GeometryNodeAccumulateField"); acc.location=(760, 220) acc.data_type='FLOAT'; acc.domain='FACE' one = n.new("ShaderNodeValue"); one.location=(580, 180); one.outputs[0].default_value=1.0 L.new(one.outputs[0], acc.inputs["Value"]) store = n.new("GeometryNodeStoreNamedAttribute"); store.location=(1120, 220) store.data_type='FLOAT'; store.domain='FACE'; store.inputs["Name"].default_value="ray_density" L.new(acc.outputs["Total"], store.inputs["Value"]) # Join Geometry optional join = n.new("GeometryNodeJoinGeometry"); join.location=(1260, 520) L.new(inst.outputs["Instances"], join.inputs["Geometry"]) base_in = n.new("NodeGroupInput"); base_in.location=(100, -80) # implicit geometry input L.new(base_in.outputs[0], m2p.inputs["Mesh"]) L.new(base_in.outputs[0], join.inputs["Geometry"]) sw = n.new("GeometryNodeSwitch"); sw.location=(1420, 520); sw.input_type='GEOMETRY' L.new(gi.outputs["OutputLines"], sw.inputs[0]) L.new(join.outputs["Geometry"], sw.inputs[15]) # true → mit Linien L.new(store.outputs["Geometry"], sw.inputs[14]) # false → nur Attribut L.new(sw.outputs["Output"], n.new("NodeGroupOutput").inputs["Geometry"]) return ng def add_ray_sampler_modifier(obj, camera=None, rays_per_face=12, jitter_deg=5.0, seed=1, ray_len=0.35, output_lines=True): ng = make_geo_ray_sampler() mod = obj.modifiers.get("Gem_RaySampler_v3") or obj.modifiers.new("Gem_RaySampler_v3", type='NODES') mod.node_group = ng cam = camera or bpy.context.scene.camera or next((o for o in bpy.data.objects if o.type=='CAMERA'), None) if cam: mod["Input_1"] = cam try: mod["Input_2"] = float(ray_len) mod["Input_3"] = int(rays_per_face) mod["Input_4"] = float(jitter_deg) mod["Input_5"] = int(seed) mod["Input_6"] = bool(output_lines) except Exception: pass return mod # ========================= # Heatmap Material # ========================= def assign_heatmap_material(obj): mat = bpy.data.materials.get("Gem_Heatmap") or bpy.data.materials.new("Gem_Heatmap") mat.use_nodes = True nt = mat.node_tree; nt.nodes.clear() outp = nt.nodes.new("ShaderNodeOutputMaterial"); outp.location=(640,0) em = nt.nodes.new("ShaderNodeEmission"); em.location=(420,-20) ramp = nt.nodes.new("ShaderNodeValToRGB"); ramp.location=(180,0) attr = nt.nodes.new("ShaderNodeAttribute"); attr.location=(-80,0); attr.attribute_name="ray_density" cr = ramp.color_ramp; cr.interpolation='EASE' while len(cr.elements)>2: cr.elements.remove(cr.elements[-1]) cr.elements[0].position=0.0; cr.elements[0].color=(0.1,0.2,0.95,1) e2 = cr.elements.new(0.25); e2.color=(0.0,0.85,0.8,1) e3 = cr.elements.new(0.5 ); e3.color=(0.98,0.86,0.15,1) e4 = cr.elements.new(0.75); e4.color=(0.98,0.55,0.05,1) cr.elements[1].position=1.0; cr.elements[1].color=(0.95,0.1,0.05,1) link(nt, attr, "Fac", ramp, "Fac") link(nt, ramp, "Color", em, "Color") em.inputs["Strength"].default_value = 2.0 link(nt, em, "Emission", outp, "Surface") obj.data.materials.clear(); obj.data.materials.append(mat) return mat # ========================= # Facet Picker (Klick → highlight) # ========================= class GEM_OT_pick_facet(bpy.types.Operator): bl_idname = "gem.pick_facet" bl_label = "Pick Facet (Gem)" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): if event.type == 'LEFTMOUSE' and event.value=='PRESS': region = context.region rv3d = context.region_data coord = (event.mouse_region_x, event.mouse_region_y) obj = context.active_object if not obj or obj.type!='MESH': return {'RUNNING_MODAL'} from bpy_extras import view3d_utils origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord).normalized() success, loc, normal, index = obj.ray_cast(origin, direction) if success: me = obj.data bm = bmesh.new(); bm.from_mesh(me); bm.faces.ensure_lookup_table() for f in bm.faces: f.select=False if index < len(bm.faces): bm.faces[index].select=True bm.to_mesh(me); bm.free() self.report({'INFO'}, f"Facet index: {index}") return {'RUNNING_MODAL'} if event.type in {'ESC','RIGHTMOUSE'}: return {'FINISHED'} return {'RUNNING_MODAL'} # ========================= # UI Panel (N-Panel → GEM) # ========================= class GEM_PT_panel(bpy.types.Panel): bl_label = "Gem I Like Toolkit" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'GEM' def draw(self, ctx): layout = self.layout scn = ctx.scene col = layout.column(align=True) col.label(text="Ray Sampler") col.prop(scn, "gem_rays_per_face") col.prop(scn, "gem_jitter_deg") col.prop(scn, "gem_seed") col.prop(scn, "gem_ray_len") col.prop(scn, "gem_output_lines") col.operator("gem.apply_rays", icon="MOD_NODES") layout.separator() col = layout.column(align=True) col.label(text="Dispersion (Sellmeier)") col.prop(scn, "gem_material_preset") col.prop(scn, "gem_rough") col.prop(scn, "gem_absorption_density") col.operator("gem.apply_dispersion", icon="MATERIAL") layout.separator() col = layout.column(align=True) col.label(text="Heatmap / Facets") col.operator("gem.assign_heatmap", icon="SHADING_TEXTURE") col.operator("gem.pick_facet", icon="RESTRICT_SELECT_OFF") layout.separator() col = layout.column(align=True) col.label(text="ASC/.gem Round-Trip") col.operator("gem.import_file", icon="IMPORT") col.operator("gem.export_asc", icon="EXPORT") col.operator("gem.export_gem", icon="EXPORT") layout.separator() col = layout.column(align=True) col.label(text="Beispielszene / .blend") col.operator("gem.setup_example", icon="OUTLINER_OB_LIGHT") col.operator("gem.save_blend", icon="FILE_TICK") class GEM_OT_apply_rays(bpy.types.Operator): bl_idname = "gem.apply_rays" bl_label = "Apply Ray Sampler to Active" def execute(self, ctx): o = ctx.active_object if not o or o.type!='MESH': self.report({'ERROR'}, "Aktives Objekt ist kein Mesh.") return {'CANCELLED'} cam = ctx.scene.camera add_ray_sampler_modifier( o, camera=cam, rays_per_face=ctx.scene.gem_rays_per_face, jitter_deg=ctx.scene.gem_jitter_deg, seed=ctx.scene.gem_seed, ray_len=ctx.scene.gem_ray_len, output_lines=ctx.scene.gem_output_lines ) 🔧 Verwendung .asc importieren & alles setzen: Code kopieren Python path = r"/Pfad/zu/deinem_schliff.asc" obj = demo_setup(path) Heatmap aktivieren (anstatt Glas-Material): Code kopieren Python obj = bpy.context.active_object obj.data.materials.clear() obj.data.materials.append(make_heatmap_material()) Ray-Sampler feintunen: Im Modifier „RayFan_v2“ am Objekt: RaysPerFace (aktuell ein Platzhalter, da Geo Nodes ohne Repeat-Zone schwer zu duplizieren sind; die Instanzierung erfolgt 1x/Face – fĂŒr echte N-Samples bitte Blender 4.x „Repeat Zone“ nutzen oder denselben Modifier mehrfach duplizieren) JitterDeg (Streuung um den Reflexionsvektor) RayLength (LinienlĂ€nge) Hinweis zum Sampler: Die bereitgestellte Node-Konfiguration erzeugt aktuell 1 Strahl pro Facette mit Jitter; fĂŒr echtes „N pro Face“ brauchst du in 4.x eine Repeat Zone in Geo Nodes oder duplizierst den Modifier mehrmals mit verschiedenen Seeds. Ich kann dir gern eine 4.x-Repeat-Zone-Version aufsetzen, wenn du willst. ✅ Was jetzt funktioniert Kamera→Facettenzentrum als Einfallsvektor Reflexion an der Facettennormalen Strahl-Visualisierung (Linie) Heatmap-Material ĂŒber Attribut ray_density (aktuell als einfache 1-Gewichtung; mit Repeat Zone kann man echte Dichte akkumulieren) ASC-Import mit facet_id auf Face-Domain ▶ NĂ€chste Ausbauten (ich kann sie fĂŒr dich ergĂ€nzen) Repeat-Zone Ray-Sampler (Blender ≄4.0): N einstellbar, echte Accumulate Field → ray_density Dispersion-Shader Node-Group (physikalisch nĂ€her an Abbe-Zahl) ASC-Importer-Robustheit (weitere Varianten / .gem Parser) Facet-ID-Farben direkt im Viewport + Selektions-Operator (Klick → Facette hervorheben) Exporter fĂŒr Lichtweg-Statistiken (CSV/JSON)