Skip to content

Instantly share code, notes, and snippets.

@tako2
Created November 26, 2025 11:29
Show Gist options
  • Select an option

  • Save tako2/5ebd2a4a34d42b5aae6ce7a2af4a1c75 to your computer and use it in GitHub Desktop.

Select an option

Save tako2/5ebd2a4a34d42b5aae6ce7a2af4a1c75 to your computer and use it in GitHub Desktop.
#!/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