""" raymarch toy usage: march ... ::= "." | ::= | "(" ")" example: march cube.pos(0, 0, 0).color(red) cam(1.0).pos(1, 1, 1).lookat(0, 0, 0) """ import pathlib from dataclasses import dataclass import re import numpy as np from dataclasses import dataclass from typing import Optional from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES from konabot.plugins.marchtoy.op import OPERATION_ENTRIES @dataclass class Command: id: str args: list[str] class CommandChainParser: CHAIN_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?(\.[a-zA-Z]+(\([^(]*\))?)*" def __init__(self, _command_chain: str) -> None: self.command_chain = _command_chain def __iter__(self): return self def __next__(self): if query := re.match(CommandChainParser.CHAIN_PATTERN, self.command_chain): cmd_chain = query[0] self.command_chain = self.command_chain[len(cmd_chain) + 1 :] return cmd_chain raise StopIteration class CommandParser: CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" def __init__(self, _command: str) -> None: self.command = _command def __iter__(self): return self def __next__(self): if query := re.match(CommandParser.CMD_PATTERN, self.command): cmd = query[0] if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd): cmd_id = cmd_id_qry[0] self.command = self.command[len(cmd) + 1 :] cmd_args = cmd[len(cmd_id) + 1 : -1].replace(" ", "").split(",") while "" in cmd_args: cmd_args.remove("") return Command(cmd_id, cmd_args) raise StopIteration class Scene: def __init__(self, _instruction: str) -> None: self.canvas_objs: list[tuple[Object, str]] = [] self.camera: Camera = Camera() for raw_cmd in CommandChainParser(_instruction): cmd_queue = CommandParser(raw_cmd) cmd_obj = next(cmd_queue) obj_id, obj_args = cmd_obj.id, cmd_obj.args obj_instance: Optional[Object] = None if obj_id in OBJECT_ENTRIES: obj_cls = OBJECT_ENTRIES[obj_id] if not issubclass(obj_cls, Object): raise Exception(f"{obj_id} is not a subclass of Object.") obj_instance = obj_cls() try: if len(obj_args) != 0: obj_instance.parse_args(obj_args) except Exception as e: raise Exception( f"object {obj_id} failed to parse args passed in: {obj_args}" ) from e else: raise Exception(f"{obj_id} is not a valid object type.") if obj_instance != None: for cmd in cmd_queue: op_id, op_args = cmd.id, cmd.args if op_id in OPERATION_ENTRIES: op_func = OPERATION_ENTRIES[op_id] if not callable(op_func): raise Exception(f"{op_id} is not a valid operation.") op_func(obj_instance, op_args) else: raise Exception(f"{op_id} is not a valid operation.") try: sdf_block = obj_instance.sdf_block() self.canvas_objs.append((obj_instance, sdf_block)) except: if type(obj_instance) == Camera: self.camera = obj_instance def __str__(self) -> str: return ", ".join([str(type(obj)) for obj in self.canvas_objs]) def compile(self, fs_src: str = "frag.glsl") -> str: PATH = pathlib.Path(__file__).parent / "shaders" / fs_src with PATH.open(encoding="utf-8") as f: content = f.read() sdf_block: str = "" color_block: str = "" index = 0 for canvas_item in self.canvas_objs: obj, sdf_expr = canvas_item sdf_block += f"float sd{index} = {sdf_expr};" sdf_block += f"if(sd{index} < qry.value)" sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" color = obj.texture.color color_block += ( f"if(obj_id == {index}) return float4(" f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n" ) index += 1 content = content.replace("", sdf_block) content = content.replace("", color_block) cam_pos = self.camera.transform.t[0:3, 3] cam_focus = self.camera.focus cam_dir = self.camera.transform.t @ np.array((0.0, 0.0, -1.0, 0.0)) content = content.replace( "", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}" ) content = content.replace("", str(cam_focus)) content = content.replace( "", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}" ) return content