204 lines
5.8 KiB
Python
204 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import re
|
|
from math import cos, sin
|
|
from typing import cast
|
|
import dearpygui.dearpygui as dpg # pyright: ignore[reportMissingTypeStubs]
|
|
|
|
RENDER_VERTICES = True
|
|
RENDER_EDGES = True
|
|
BEST_EFFORT_PARSE = True
|
|
|
|
CANVAS_SIZE = 500
|
|
TIMESTEP = 0.01
|
|
CAMERA_DISTANCE = 2.5
|
|
|
|
VERTEX_SIZE = 5.0
|
|
VERTEX_COLOR = (27, 188, 104)
|
|
EDGE_SIZE = 1.0
|
|
EDGE_COLOR = (20, 133, 38)
|
|
|
|
Vertex2 = tuple[float, float]
|
|
Vertex2Set = list[Vertex2]
|
|
Vertex3 = tuple[float, float, float]
|
|
Vertex3Set = list[Vertex3]
|
|
IndexedFace = tuple[int, int, int, int]
|
|
IndexedFaceSet = list[IndexedFace]
|
|
|
|
|
|
# 3D Space [-1, 1]x[-1, 1]x[-1, 1] -> 2D ImagePlane [-1, 1]x[-1, 1] -> Viewport in [0, WIDTH]x[0, HEIGHT]
|
|
|
|
|
|
def to_viewport(verts: Vertex2Set) -> Vertex2Set:
|
|
"""Maps vertices into the [0, WIDTH]x[0, HEIGHT] viewport"""
|
|
|
|
def transform(vert: Vertex2) -> Vertex2:
|
|
return (
|
|
((vert[0] + 1.0) / 2.0) * CANVAS_SIZE,
|
|
(1 - (vert[1] + 1.0) / 2.0) * CANVAS_SIZE,
|
|
)
|
|
|
|
return list(map(transform, verts))
|
|
|
|
|
|
def to_imageplane(verts: Vertex3Set) -> Vertex2Set:
|
|
"""Projects vertices onto the [-1, 1]x[-1, 1] imageplane"""
|
|
|
|
def transform(vert: Vertex3) -> Vertex2:
|
|
return (vert[0] / vert[2], vert[1] / vert[2])
|
|
|
|
return list(map(transform, verts))
|
|
|
|
|
|
def translate(verts: Vertex3Set, distance: float) -> Vertex3Set:
|
|
"""Translates vertices along the forward-axis in 3D space"""
|
|
|
|
def transform(vert: Vertex3) -> Vertex3:
|
|
return (vert[0], vert[1], vert[2] + distance)
|
|
|
|
return list(map(transform, verts))
|
|
|
|
|
|
def rotate(verts: Vertex3Set, angle: float) -> Vertex3Set:
|
|
"""Rotates vertices around the up-axis in 3D space"""
|
|
|
|
def transform(vert: Vertex3) -> Vertex3:
|
|
return (
|
|
vert[0] * cos(angle) - vert[2] * sin(angle),
|
|
vert[1],
|
|
vert[0] * sin(angle) + vert[2] * cos(angle),
|
|
)
|
|
|
|
return list(map(transform, verts))
|
|
|
|
|
|
def draw(drawlist, vertices: Vertex3Set, faces: IndexedFaceSet, time: float):
|
|
dpg.delete_item(drawlist, children_only=True)
|
|
|
|
def draw_vertex(vert: Vertex2) -> None:
|
|
_ = dpg.draw_circle(
|
|
center=(vert[0], vert[1]),
|
|
radius=VERTEX_SIZE,
|
|
parent=drawlist,
|
|
fill=VERTEX_COLOR,
|
|
color=VERTEX_COLOR,
|
|
)
|
|
|
|
def draw_line(vertA: Vertex2, vertB: Vertex2) -> None:
|
|
_ = dpg.draw_line(
|
|
p1=(vertA[0], vertA[1]),
|
|
p2=(vertB[0], vertB[1]),
|
|
parent=drawlist,
|
|
color=EDGE_COLOR,
|
|
thickness=EDGE_SIZE,
|
|
)
|
|
|
|
rotated_vertices: Vertex3Set = rotate(vertices, time)
|
|
translated_vertices: Vertex3Set = translate(rotated_vertices, CAMERA_DISTANCE)
|
|
imageplane_vertices: Vertex2Set = to_imageplane(translated_vertices)
|
|
viewport_vertices: Vertex2Set = to_viewport(imageplane_vertices)
|
|
|
|
if RENDER_VERTICES:
|
|
for v in viewport_vertices:
|
|
draw_vertex(v)
|
|
|
|
if RENDER_EDGES:
|
|
for f in faces:
|
|
v0: Vertex2 = viewport_vertices[f[0]]
|
|
v1: Vertex2 = viewport_vertices[f[1]]
|
|
v2: Vertex2 = viewport_vertices[f[2]]
|
|
v3: Vertex2 = viewport_vertices[f[3]]
|
|
|
|
draw_line(v0, v1)
|
|
draw_line(v1, v2)
|
|
draw_line(v2, v3)
|
|
draw_line(v3, v0)
|
|
|
|
|
|
def parse_obj_file(path: str) -> tuple[Vertex3Set, IndexedFaceSet]:
|
|
with open(path, "r") as file:
|
|
lines = file.readlines()
|
|
|
|
vertices: Vertex3Set = []
|
|
faces: IndexedFaceSet = []
|
|
|
|
for line in lines:
|
|
if line.startswith("v "):
|
|
|
|
matches = re.findall(r"^v (.*?) (.*?) (.*?)$", line)
|
|
if len(matches) != 1:
|
|
if BEST_EFFORT_PARSE:
|
|
continue
|
|
raise RuntimeError("Failed to parse .obj file")
|
|
|
|
x, y, z = matches[0]
|
|
vertices += [(float(x), float(y), float(z))]
|
|
|
|
elif line.startswith("f "):
|
|
|
|
matches = re.findall(r"^f (.*?) (.*?) (.*?) (.*?)$", line)
|
|
if len(matches) != 1:
|
|
if BEST_EFFORT_PARSE:
|
|
continue
|
|
raise RuntimeError("Failed to parse .obj file")
|
|
|
|
v0, v1, v2, v3 = matches[0]
|
|
v0 = int(v0) - 1
|
|
v1 = int(v1) - 1
|
|
v2 = int(v2) - 1
|
|
v3 = int(v3) - 1
|
|
faces += [(v0, v1, v2, v3)]
|
|
|
|
return vertices, faces
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
_ = parser.add_argument("objfile", type=str, help=".obj file to render")
|
|
_ = parser.add_argument("--vertices", action="store_true", help="render vertices")
|
|
_ = parser.add_argument("--edges", action="store_true", help="render edges")
|
|
_ = parser.add_argument(
|
|
"--strict", action="store_true", help="require complete parsing of .obj file"
|
|
)
|
|
args = parser.parse_args()
|
|
args.objfile = cast(str, args.objfile)
|
|
|
|
global RENDER_VERTICES
|
|
global RENDER_EDGES
|
|
global BEST_EFFORT_PARSE
|
|
|
|
RENDER_VERTICES = bool(args.vertices)
|
|
RENDER_EDGES = bool(args.edges)
|
|
BEST_EFFORT_PARSE = not bool(args.strict)
|
|
|
|
dpg.create_context()
|
|
|
|
with dpg.theme() as canvas_theme, dpg.theme_component():
|
|
_ = dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0)
|
|
|
|
with dpg.window(label="Obj Render") as canvas:
|
|
dpg.bind_item_theme(canvas, canvas_theme)
|
|
dpg.set_primary_window(dpg.last_item(), True)
|
|
|
|
with dpg.drawlist(width=CANVAS_SIZE, height=CANVAS_SIZE) as d:
|
|
drawlist = d
|
|
|
|
dpg.create_viewport(title="Obj Render", width=CANVAS_SIZE, height=CANVAS_SIZE)
|
|
dpg.setup_dearpygui()
|
|
dpg.show_viewport()
|
|
|
|
vertices, faces = parse_obj_file(args.objfile)
|
|
|
|
time: float = 0
|
|
while dpg.is_dearpygui_running():
|
|
draw(drawlist, vertices, faces, time)
|
|
dpg.render_dearpygui_frame()
|
|
time += TIMESTEP
|
|
|
|
dpg.destroy_context()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|