import numpy as np import time import math import os # Importiert für die Verzeichniserstellung from PIL import Image, ImageDraw # Importiert für die Bildverarbeitung # --- Hilfsfunktionen --- def normalize(v): """ Normalisiert einen Vektor (macht seine Länge zu 1). """ norm = np.linalg.norm(v) if norm == 0: return v return v / norm def look_at_matrix(eye, target, up): """ Erzeugt eine View-Matrix, die Weltkoordinaten in Kamerakoordinaten transformiert. :param eye: Position der Kamera (np.array). :param target: Punkt, auf den die Kamera schaut (np.array). :param up: Up-Vektor der Kamera (zeigt, wo "oben" für die Kamera ist, np.array). :return: 4x4 View-Matrix (NumPy-Array). """ # Kamerakoordinatensystem berechnen forward = normalize(eye - target) # Konvention: Kamera schaut entlang -Z if np.linalg.norm(forward) < 1e-9: # Verhindert Division durch Null oder NaN, wenn eye==target # Wenn Kamera und Ziel am selben Ort sind, ist die Richtung undefiniert. # Wähle eine Standardrichtung, z.B. entlang -Z der Welt. forward = np.array([0.0, 0.0, 1.0]) # Prüfe, ob 'up' und 'forward' (fast) parallel sind if abs(np.dot(normalize(up), forward)) > 0.999: # Fallback, wenn 'up' und 'forward' (fast) parallel sind # Wähle einen anderen temporären Up-Vektor if abs(forward[1]) < 0.99: # Wenn forward nicht fast vertikal ist temp_up = np.array([0.0, 1.0, 0.0]) else: # Wenn forward fast vertikal ist temp_up = np.array([0.0, 0.0, -1.0 if forward[1] > 0 else 1.0]) right = normalize(np.cross(temp_up, forward)) else: right = normalize(np.cross(up, forward)) # Den tatsächlichen Up-Vektor der Kamera neu berechnen, damit er orthogonal ist camera_up = normalize(np.cross(forward, right)) # Rotationsmatrix erstellen rotation = np.identity(4) rotation[0, 0:3] = right rotation[1, 0:3] = camera_up rotation[2, 0:3] = forward # Translationsmatrix erstellen translation = np.identity(4) translation[0:3, 3] = -eye # View-Matrix: Zuerst Translation, dann Rotation view_mat = np.dot(rotation, translation) return view_mat def perspective_projection(point_cam_space, fov_deg, aspect_ratio, near, far): """ Projiziert einen Punkt aus dem Kamera-Koordinatenraum auf eine 2D-Ebene. Gibt normalisierte Gerätekoordinaten (NDC) zurück (-1 bis +1). :param point_cam_space: Punkt in Kamera-Koordinaten (3D NumPy-Array). :param fov_deg: Field of View (vertikal) in Grad. :param aspect_ratio: Seitenverhältnis (Breite / Höhe) der Bildebene. :param near: Nahe Clipping-Ebene. :param far: Ferne Clipping-Ebene. :return: Projizierter 2D-Punkt in NDC (NumPy-Array) oder None, wenn außerhalb. """ # Kamera schaut entlang -Z, Z muss negativ sein und innerhalb der Clipping-Grenzen liegen if point_cam_space[2] > -near or point_cam_space[2] < -far: return None # Außerhalb des Sichtfelds (vor near oder hinter far) # Verhindert Division durch Null, falls Punkt genau auf der Kameraebene liegt (z=0) # Obwohl das Clipping oben dies meist abfängt, ist es eine zusätzliche Sicherheit. if abs(point_cam_space[2]) < 1e-9: return None # Skalierungsfaktor basierend auf FOV f = 1.0 / math.tan(math.radians(fov_deg) / 2.0) # Projizierte Koordinaten auf der Bildebene (NDC) x_ndc = (f / aspect_ratio) * point_cam_space[0] / -point_cam_space[2] y_ndc = f * point_cam_space[1] / -point_cam_space[2] # Überprüfen, ob die projizierten Koordinaten innerhalb des NDC-Bereichs [-1, 1] liegen # Dies ist ein zusätzlicher Clipping-Schritt für die XY-Ebene if not (-1 <= x_ndc <= 1 and -1 <= y_ndc <= 1): return None # Außerhalb des seitlichen Sichtfelds return np.array([x_ndc, y_ndc]) # --- NEU: Funktion zur Umwandlung von NDC in Pixelkoordinaten --- def ndc_to_pixel(ndc_coords, width, height): """ Wandelt NDC-Koordinaten (-1 bis 1) in Pixelkoordinaten (0 bis width/height) um. """ # x_ndc = -1 -> pixel_x = 0 # x_ndc = +1 -> pixel_x = width pixel_x = (ndc_coords[0] + 1.0) / 2.0 * width # y_ndc = -1 -> pixel_y = height (unten im Bild) # y_ndc = +1 -> pixel_y = 0 (oben im Bild) # Wir müssen die Y-Koordinate invertieren, da NDC +Y oben ist, Pixel +Y aber unten. pixel_y = (1.0 - ndc_coords[1]) / 2.0 * height # In Integer umwandeln und sicherstellen, dass sie innerhalb der Grenzen liegen px = int(round(pixel_x)) py = int(round(pixel_y)) # Clipping auf Bildgrenzen (obwohl NDC-Clipping dies meist unnötig macht) px = max(0, min(width - 1, px)) py = max(0, min(height - 1, py)) return px, py # --- Simulationsparameter --- object_start_pos = np.array([0.0, 0.0, 10.0]) # Startposition des Objekts (x, y, z) radius = 5.0 angular_speed = math.radians(45) # 45 Grad pro Sekunde # Kameraeinstellungen cameras = [ { "name": "Kamera_1_Frontal", # Namen ohne Leerzeichen für Dateinamen "pos": np.array([0.0, 0.0, 0.0]), "target": object_start_pos, "up": np.array([0.0, 1.0, 0.0]), "fov_deg": 60, "aspect_ratio": 16.0 / 9.0 }, { "name": "Kamera_2_Seitlich_Links", "pos": np.array([-15.0, 0.0, 5.0]), "target": object_start_pos, "up": np.array([0.0, 1.0, 0.0]), "fov_deg": 45, "aspect_ratio": 1.0 }, { "name": "Kamera_3_Von_Oben", "pos": np.array([0.0, 15.0, 10.0]), "target": object_start_pos, "up": np.array([0.0, 0.0, -1.0]), # Blickrichtung -Y, Z nach unten ist "up" "fov_deg": 70, "aspect_ratio": 4.0 / 3.0 } ] # Clipping-Ebenen near_plane = 0.1 far_plane = 100.0 simulation_duration = 10 # Sekunden time_step = 1 # Zeitintervall für "Aufnahmen" in Sekunden # --- NEU: Bildparameter --- IMG_WIDTH = 320 # Bildbreite in Pixel IMG_HEIGHT = 180 # Bildhöhe in Pixel (Beispiel für 16:9) OUTPUT_DIR = "./bilder" # Verzeichnis für die Bilder # --- NEU: Ausgabeverzeichnis erstellen --- os.makedirs(OUTPUT_DIR, exist_ok=True) print(f"Bilder werden in '{OUTPUT_DIR}' gespeichert.") # --- Simulationsschleife --- current_object_pos = object_start_pos.copy() current_time = 0.0 angle = 0.0 # Für Kreisbewegung print("Starte Simulation...") while current_time <= simulation_duration: print(f"\n--- Zeitpunkt: {current_time:.1f}s ---") # Objektposition aktualisieren (Kreisbewegung) angle = angular_speed * current_time current_object_pos[0] = radius * math.cos(angle) + 0 current_object_pos[2] = radius * math.sin(angle) + 10 print(f"Objekt Welt-Position: ({current_object_pos[0]:.2f}, {current_object_pos[1]:.2f}, {current_object_pos[2]:.2f})") # Objektposition in homogene Koordinaten umwandeln object_pos_h = np.append(current_object_pos, 1.0) # Für jede Kamera die Ansicht berechnen und Bild erzeugen for i, cam in enumerate(cameras): print(f"\n Kamera {i+1}: {cam['name']}") print(f" Position: ({cam['pos'][0]:.2f}, {cam['pos'][1]:.2f}, {cam['pos'][2]:.2f})") # Kamera schaut immer auf die aktuelle Objektposition current_target = current_object_pos # Korrigiere das Seitenverhältnis basierend auf den Bilddimensionen # Dies überschreibt das manuell gesetzte Seitenverhältnis effective_aspect_ratio = IMG_WIDTH / IMG_HEIGHT print(f" Schaut auf (Target): ({current_target[0]:.2f}, {current_target[1]:.2f}, {current_target[2]:.2f})") print(f" Up-Vektor: ({cam['up'][0]:.2f}, {cam['up'][1]:.2f}, {cam['up'][2]:.2f})") print(f" FOV: {cam['fov_deg']} Grad, Aspekt (für Projektion): {effective_aspect_ratio:.2f}") # View-Matrix berechnen view_mat = look_at_matrix(cam['pos'], current_target, cam['up']) # Objektposition in Kamera-Koordinaten transformieren point_in_cam_space_h = np.dot(view_mat, object_pos_h) # Sicherstellen, dass w positiv ist (sollte nach View-Transform 1 sein) if point_in_cam_space_h[3] <= 0: print(" Objekt hinter der Kamera nach View-Transformation (w <= 0).") projected_point_ndc = None point_in_cam_space = np.array([np.nan, np.nan, np.nan]) # Ungültig else: point_in_cam_space = point_in_cam_space_h[:3] / point_in_cam_space_h[3] print(f" Objekt in Kamera-Koordinaten: ({point_in_cam_space[0]:.2f}, {point_in_cam_space[1]:.2f}, {point_in_cam_space[2]:.2f})") # In 2D projizieren (NDC) projected_point_ndc = perspective_projection( point_in_cam_space, cam['fov_deg'], effective_aspect_ratio, # Benutze das tatsächliche Bildseitenverhältnis near_plane, far_plane ) # --- NEU: Bild erstellen und speichern --- # Erstelle ein neues schwarzes Bild # Passe Höhe ggf. an, wenn das Seitenverhältnis der Kamera nicht zum globalen passt # Hier verwenden wir globale Höhe, aber berechnen den Aspekt neu. # Alternativ könnte man Höhe/Breite pro Kamera anpassen. img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color='black') draw = ImageDraw.Draw(img) if projected_point_ndc is not None: print(f" Projizierte 2D-Koordinaten (NDC): ({projected_point_ndc[0]:.3f}, {projected_point_ndc[1]:.3f})") # NDC in Pixelkoordinaten umwandeln px, py = ndc_to_pixel(projected_point_ndc, IMG_WIDTH, IMG_HEIGHT) print(f" Pixelkoordinaten (im Bild): ({px}, {py})") # Einen weißen Punkt (oder kleines Quadrat für bessere Sichtbarkeit) zeichnen # draw.point((px, py), fill='white') # Einzelner Pixel draw.rectangle([(px-1, py-1), (px+1, py+1)], fill='white', outline='white') # 3x3 Quadrat else: print(" Objekt befindet sich ausserhalb des Sichtbereichs (Clipping).") # Das Bild bleibt schwarz, da nichts gezeichnet wird. # Bild speichern timestamp_str = f"{current_time:.1f}".replace('.', '_') # Punkt im Dateinamen vermeiden filename = f"{cam['name']}_t_{timestamp_str}.png" filepath = os.path.join(OUTPUT_DIR, filename) img.save(filepath) print(f" Bild gespeichert: {filepath}") # Zum nächsten Zeitschritt gehen current_time += time_step if current_time <= simulation_duration: # Kurze Pause zur besseren Lesbarkeit der Ausgabe (optional) # time.sleep(0.1) # Reduziert, um die Simulation zu beschleunigen pass # Keine Pause für schnellere Ausführung print("\nSimulation beendet.")