diff --git a/main.py b/main.py new file mode 100644 index 0000000..08a1b2c --- /dev/null +++ b/main.py @@ -0,0 +1,181 @@ +#!/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 = False +RENDER_EDGES = True +BEST_EFFORT_PARSE = True + +CANVAS_SIZE = 500 +TIMESTEP = 0.01 +CAMERA_DISTANCE = 3.0 + +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: + 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: + 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: + 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: + 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, + ) + + viewport_vertices: Vertex2Set = to_viewport( + to_imageplane(translate(rotate(vertices, time), CAMERA_DISTANCE)) + ) + + 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") + args = parser.parse_args() + args.objfile = cast(str, args.objfile) + + 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()