Created
November 26, 2025 11:29
-
-
Save tako2/5ebd2a4a34d42b5aae6ce7a2af4a1c75 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| import pyxel | |
| import math | |
| import json | |
| import sys | |
| COLORS = [0x000000, 0x0000FF, 0xFF0000, 0xFF00FF, | |
| 0x00FF00, 0x00FFFF, 0xFFFF00, 0xFFFFFF, | |
| 0x444444, 0x000088, 0x880000, 0x880088, | |
| 0x008800, 0x008888, 0x888800, 0x888888] | |
| def get_color(rgb): | |
| col = 0 | |
| if rgb[0] > 0.5: | |
| if rgb[1] > 0.5: | |
| if rgb[2] > 0.5: | |
| col = 7 | |
| else: | |
| col = 6 | |
| else: | |
| if rgb[2] > 0.5: | |
| col = 3 | |
| else: | |
| col = 2 | |
| else: | |
| if rgb[1] > 0.5: | |
| if rgb[2] > 0.5: | |
| col = 5 | |
| else: | |
| col = 4 | |
| else: | |
| if rgb[2] > 0.5: | |
| col = 1 | |
| else: | |
| col = 0 | |
| return col | |
| # コマンドライン引数からファイル名を取得 | |
| if len(sys.argv) < 2: | |
| print("使い方: python view_wireframe.py [JSONファイル]") | |
| sys.exit(1) | |
| filename = sys.argv[1] | |
| def load_model(path): | |
| with open(path, "r", encoding="utf-8") as f: | |
| json_data = json.load(f) | |
| return json_data["objects"] | |
| model_objs = load_model(filename) | |
| for obj in model_objs: | |
| for v in obj["vertices"]: | |
| v[0] = -v[0] | |
| #------------------------------------------------------------------------------ | |
| # 回転角度 | |
| angle_x, angle_y = math.pi / 9, math.pi | |
| dist = 8.0 | |
| #------------------------------------------------------------------------------ | |
| def rotate_vertex(v, ax, ay): | |
| """X軸・Y軸回転を適用した頂点を返す""" | |
| x, y, z = v | |
| # Y軸回転 | |
| cos_y, sin_y = math.cos(ay), math.sin(ay) | |
| x, z = x * cos_y - z * sin_y, x * sin_y + z * cos_y | |
| # X軸回転 | |
| cos_x, sin_x = math.cos(ax), math.sin(ax) | |
| y, z = y * cos_x - z * sin_x, y * sin_x + z * cos_x | |
| return (x, y, z) | |
| #------------------------------------------------------------------------------ | |
| def project_vertex(v): | |
| """透視投影""" | |
| x, y, z = v | |
| #dist = 8.0 # 視点距離 | |
| f = 100 / (z + dist) # スケーリング係数 | |
| return (int(100 + x * f), int(100 - y * f), z) # 画面中央に配置 | |
| #------------------------------------------------------------------------------ | |
| def draw_model_wf(obj): | |
| # 頂点を回転+投影 | |
| transformed = [ | |
| project_vertex(rotate_vertex(v, angle_x, angle_y)) for v in obj["vertices"] | |
| ] | |
| col = get_color(obj["material_color"]) | |
| # エッジを線で描画 | |
| for e in obj["edges"]: | |
| v1, v2 = transformed[e[0]], transformed[e[1]] | |
| pyxel.line(v1[0], v1[1], v2[0], v2[1], col) | |
| # ==== 4x4 Bayer マトリクス ==== | |
| BAYER4 = [ | |
| [0, 8, 2, 10], | |
| [12, 4, 14, 6], | |
| [3, 11, 1, 9], | |
| [15, 7, 13, 5], | |
| ] | |
| # ==== ディザ塗り潰し ==== | |
| def draw_filled_polygon(points, intensity, col1=7, col2=0): | |
| min_y = max(min(p[1] for p in points), 0) | |
| max_y = min(max(p[1] for p in points), pyxel.height - 1) | |
| for y in range(min_y, max_y + 1): | |
| nodes = [] | |
| j = len(points) - 1 | |
| for i in range(len(points)): | |
| x0, y0, _ = points[i] | |
| x1, y1, _ = points[j] | |
| if (y0 < y and y1 >= y) or (y1 < y and y0 >= y): | |
| x_int = int(x0 + (y - y0) * (x1 - x0) / (y1 - y0)) | |
| nodes.append(x_int) | |
| j = i | |
| nodes.sort() | |
| for n in range(0, len(nodes), 2): | |
| if n+1 < len(nodes): | |
| x_start, x_end = nodes[n], nodes[n+1] | |
| for x in range(x_start, x_end + 1): | |
| if 0 <= x < pyxel.width: | |
| if BAYER4[y % 4][x % 4] < intensity: | |
| pyxel.pset(x, y, col1) | |
| else: | |
| pyxel.pset(x, y, col2) | |
| #------------------------------------------------------------------------------ | |
| def draw_polygon(points, intensity, col1=7, col2=0): | |
| pt0 = None | |
| pt1 = None | |
| for pt in points: | |
| if pt0 == None: | |
| pt0 = pt | |
| if pt1 != None: | |
| pyxel.line(pt1[0], pt1[1], pt[0], pt[1], col1) | |
| pt1 = pt | |
| pyxel.line(pt0[0], pt0[1], pt1[0], pt1[1], col1) | |
| #------------------------------------------------------------------------------ | |
| def draw_model_obj(obj): | |
| # 投影済み座標 | |
| projected = [ | |
| project_vertex(rotate_vertex(v, angle_x, angle_y)) for v in obj["vertices"] | |
| ] | |
| #cam_dir = (0, 0, 1) # カメラは z+ 向き | |
| cam_dir = (0, 0, -1) # カメラは z+ 向き | |
| l_dir = (0, 1 / math.sqrt(2), -1 / math.sqrt(2)) | |
| # フェイスごとの描画データを作成 | |
| draw_faces = [] | |
| for face, normal in zip(obj["faces"], obj["normals"]): | |
| # 法線も回転 | |
| nx, ny, nz = rotate_vertex(normal, angle_x, angle_y) | |
| # 内積で背面カリング | |
| dot = nx * cam_dir[0] + ny * cam_dir[1] + nz * cam_dir[2] | |
| if dot <= 0: | |
| continue | |
| pts = [projected[i] for i in face] | |
| # 内積の強さを 0〜15 にスケーリング | |
| intensity = nx * l_dir[0] + ny * l_dir[1] + nz * l_dir[2] | |
| intensity = min(max(int(intensity * 15), 0), 15) | |
| if intensity < 2: | |
| intensity = 2 | |
| # 平均Zを計算(Painter's Algorithm 用) | |
| arr_z = [pt[2] for pt in pts] | |
| avg_z = sum(arr_z) / len(arr_z) | |
| draw_faces.append((avg_z, pts, intensity)) | |
| # Zソート(奥から手前へ) | |
| draw_faces.sort(key=lambda item: item[0], reverse=True) | |
| col = get_color(obj["material_color"]) | |
| for _, pts, intensity in draw_faces: | |
| draw_filled_polygon(pts, intensity, col1=col, col2=0) | |
| #draw_polygon(pts, intensity, col1=col, col2=0) | |
| class App: | |
| def __init__(self): | |
| self.polygon = 2 | |
| self.wheel = 0 | |
| pyxel.init(200, 200, title=f"Wireframe Viewer - {filename}") | |
| pyxel.colors.from_list(COLORS) | |
| pyxel.run(self.update, self.draw) | |
| def update(self): | |
| global angle_x, angle_y, dist | |
| if pyxel.btn(pyxel.KEY_LEFT): | |
| angle_y -= 0.1 | |
| if pyxel.btn(pyxel.KEY_RIGHT): | |
| angle_y += 0.1 | |
| if pyxel.btn(pyxel.KEY_UP): | |
| angle_x -= 0.1 | |
| if pyxel.btn(pyxel.KEY_DOWN): | |
| angle_x += 0.1 | |
| if pyxel.btn(pyxel.KEY_X): | |
| dist += 0.1 | |
| if pyxel.btn(pyxel.KEY_Z): | |
| dist -= 0.1 | |
| if pyxel.mouse_wheel != 0: | |
| self.wheel = pyxel.mouse_wheel | |
| angle_y -= (pyxel.mouse_wheel / 32) * math.pi | |
| if pyxel.btnp(pyxel.KEY_SPACE): | |
| self.polygon += 1 | |
| if self.polygon > 2: | |
| self.polygon = 0 | |
| def draw(self): | |
| pyxel.cls(0) | |
| pyxel.text(20, 20, f"{self.wheel}", 7) | |
| for obj in model_objs: | |
| if self.polygon >= 1 and len(obj["faces"]) > 0: | |
| draw_model_obj(obj) | |
| if self.polygon < 2 or len(obj["faces"]) == 0: | |
| draw_model_wf(obj) | |
| App() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment