
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.
v i f).Canvas.Python 3 (dowolna aktualna wersja 3.x) — brak zewnętrznych bibliotek.
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()
Ł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.
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.
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.
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.