Przeglądarka siatki 3d z plików .OBJ w Python Tkinter

Ten poradnik pokazuje jak zbudować minimalny, niezależny od zewnętrznych bibliotek program w Pythonie z użyciem Tkinter, który ładuje geometrię z prostego pliku .obj, wykonuje ortograficzną projekcję i ciągły obrót siatki wokół osi Y. Zawiera kompletny, uruchamialny skrypt oraz objaśnienia najważniejszych fragmentów.

Co zbudujesz

Wymagania

Python 3 (dowolna aktualna wersja 3.x) — brak zewnętrznych bibliotek.

Pełny skrypt Pythona

Skopiuj poniższy kod do pliku o nazwie przegladarka.py lub ściągnij stąd: przegladarka.py i uruchom go poleceniem python przegladarka.py.

#!/usr/bin/env python3
"""
Przeglądarka siatki przewodowej OBJ w Tkinter obracająca model wokół osi Y.
Obsługuje linie 'v x y z' (wierzchołki) oraz 'f i j k ...' (ściany), indeksy 1-based w pliku OBJ.
"""

import math
import tkinter as tk
from tkinter import filedialog, messagebox

# ---------- Ładowarka OBJ ----------
def load_obj(path):
    verts = []
    faces = []
    with open(path, 'r') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            parts = line.split()
            if parts[0] == 'v' and len(parts) >= 4:
                x, y, z = map(float, parts[1:4])
                verts.append((x, y, z))
            elif parts[0] == 'f' and len(parts) >= 4:
                idxs = []
                for p in parts[1:]:
                    # obsługa formatu 'f v/tc/vn' — bierzemy pierwszą część przed '/'
                    i = p.split('/')[0]
                    idxs.append(int(i) - 1)  # konwersja na indeks 0-based
                faces.append(idxs)
    return verts, faces

# ---------- Transformacje 3D i projekcja ----------
def rotate_y(vertex, angle_rad):
    x, y, z = vertex
    c = math.cos(angle_rad)
    s = math.sin(angle_rad)
    xr = x * c + z * s
    yr = y
    zr = -x * s + z * c
    return (xr, yr, zr)

def project_orthographic(vertex, scale, center):
    x, y, z = vertex
    sx = center[0] + x * scale
    sy = center[1] - y * scale  # odwrócenie osi Y dla współrzędnych ekranu
    return (sx, sy)

# ---------- Aplikacja przeglądarki ----------
class WireframeViewer(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Przeglądarka OBJ")
        self.geometry("900x700")

        # Płótno do rysowania
        self.canvas = tk.Canvas(self, bg='white')
        self.canvas.pack(fill=tk.BOTH, expand=True)

        # Pasek narzędzi
        toolbar = tk.Frame(self)
        toolbar.pack(side=tk.TOP, fill=tk.X)
        tk.Button(toolbar, text="Wczytaj OBJ", command=self.load).pack(side=tk.LEFT, padx=6, pady=6)
        self.pause_btn = tk.Button(toolbar, text="Pauza", command=self.toggle_pause)
        self.pause_btn.pack(side=tk.LEFT, padx=6, pady=6)
        self.speed_scale = tk.Scale(toolbar, from_=-2.0, to=2.0, resolution=0.05,
                                    orient=tk.HORIZONTAL, label="Prędkość obrotu (rad/s)")
        self.speed_scale.set(0.8)
        self.speed_scale.pack(side=tk.LEFT, padx=6, pady=6)

        # Dane modelu
        self.verts = []
        self.faces = []
        self.angle = 0.0
        self.paused = False
        self.scale_model = 1.0

        # Przerysuj przy zmianie rozmiaru
        self.bind("", lambda e: self.redraw())

        # Pętla animacji
        self.after(16, self.update_frame)

    def load(self):
        path = filedialog.askopenfilename(filetypes=[("Pliki OBJ", "*.obj"), ("Wszystkie pliki", "*.*")])
        if not path:
            return
        verts, faces = load_obj(path)
        if not verts:
            messagebox.showerror("Błąd", "Nie znaleziono wierzchołków w pliku OBJ")
            return
        self.verts = verts
        self.faces = faces
        self.center_and_scale()
        self.angle = 0.0
        self.redraw()

    def center_and_scale(self):
        xs = [v[0] for v in self.verts]
        ys = [v[1] for v in self.verts]
        zs = [v[2] for v in self.verts]
        minx, maxx = min(xs), max(xs)
        miny, maxy = min(ys), max(ys)
        minz, maxz = min(zs), max(zs)
        cx = (minx + maxx) / 2.0
        cy = (miny + maxy) / 2.0
        cz = (minz + maxz) / 2.0
        # przesuń model tak, żeby był wycentrowany w punkcie (0,0,0)
        self.verts = [(x - cx, y - cy, z - cz) for (x, y, z) in self.verts]
        span = max(maxx - minx, maxy - miny, maxz - minz, 1e-6)
        self.scale_model = 1.0 / span * 0.9  # skala, zostawiająca margines

    def toggle_pause(self):
        self.paused = not self.paused
        self.pause_btn.config(text="Wznów" if self.paused else "Pauza")

    def update_frame(self):
        if not self.paused and self.verts:
            speed = self.speed_scale.get()  # radiany na sekundę
            dt = 1.0 / 60.0
            self.angle += speed * dt
            self.redraw()
        self.after(16, self.update_frame)

    def redraw(self):
        self.canvas.delete("all")
        if not self.verts:
            w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
            self.canvas.create_text(w//2, h//2, text="Wczytaj plik OBJ, aby rozpocząć", fill="gray")
            return

        w = max(10, self.canvas.winfo_width())
        h = max(10, self.canvas.winfo_height())
        center = (w/2, h/2)
        s = min(w, h) * self.scale_model * 0.9

        # obrót wokół osi Y
        rotated = [rotate_y(v, self.angle) for v in self.verts]

        # rysuj krawędzie
        edge_color = "#000000"
        for face in self.faces:
            pts = [project_orthographic(rotated[i], s, center) for i in face]
            for a, b in zip(pts, pts[1:] + pts[:1]):
                self.canvas.create_line(a[0], a[1], b[0], b[1], fill=edge_color)

if __name__ == "__main__":
    app = WireframeViewer()
    app.mainloop()

Wyjaśnienie kluczowych sekcji

1. Ładowanie OBJ

Ładowarka czyta linie zaczynające się od v (wierzchołek) oraz f (ściana/face). Wierzchołki przechowywane są jako krotki (x,y,z), a ściany jako listy indeksów wierzchołków (indeksy 0‑based). Loader toleruje wpisy typu f 1/1 2/2 3/3 — dzieli każdy element na slash i używa pierwszej części.

2. Obrót wokół osi Y

Obrót wykonany jest za pomocą standardowej macierzy obrotu wokół osi Y. Dla kąta θ:

x' = x * cosθ + z * sinθ
y' = y
z' = -x * sinθ + z * cosθ

Zastosowanie tej transformacji do każdego wierzchołka daje współrzędne obrócone względem osi Y, używane dalej do projekcji i rysowania.

3. Projekcja i rysowanie

Projekcja ortograficzna ignoruje współrzędną Z przy mapowaniu na ekran i stosuje jednolitą skalę. Przykład wykorzystuje prosty painter's sort (średnie Z ściany) aby rysować dalsze ściany wcześniej — dzięki temu nakładanie krawędzi wygląda bardziej czytelnie dla prostych modeli.

4. Interfejs i animacja

Mały pasek narzędziowy zawiera przycisk „Load OBJ”, przycisk pauzy/wznów oraz suwak prędkości obrotu (w radianach na sekundę). Animacja używa stałego kroku czasu (1/60) wewnątrz cyklicznego wywołania after do aktualizacji kąta i przerysowania.

UV - menadżer pakietów Pythona