diff --git a/konabot/plugins/marchtoy/__init__.py b/konabot/plugins/marchtoy/__init__.py index 68acf25..38026af 100644 --- a/konabot/plugins/marchtoy/__init__.py +++ b/konabot/plugins/marchtoy/__init__.py @@ -17,4 +17,4 @@ async def _(args: Message = CommandArg()): buffer.seek(0) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) except Exception as e: - await cmd_marchtoy.send(await UniMessage.text(f"发生了错误: {e}").export()) \ No newline at end of file + await cmd_marchtoy.send(await UniMessage.text(f"cannot render: {e}").export()) \ No newline at end of file diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 005e54b..eb6ac6a 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -13,15 +13,9 @@ 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 @@ -29,7 +23,7 @@ class Command: class CommandChainParser: - CHAIN_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?(\.[a-zA-Z]+(\([^(]*\))?)*" + CHAIN_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" def __init__(self, _command_chain: str) -> None: self.command_chain = _command_chain @@ -46,7 +40,7 @@ class CommandChainParser: class CommandParser: - CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?" + CMD_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?))" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" def __init__(self, _command: str) -> None: @@ -61,88 +55,8 @@ class CommandParser: 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("") + 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_glsl() - 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 vec4(" - 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 diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 33553b4..7a22b92 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -5,7 +5,7 @@ import pathlib import moderngl import numpy as np from PIL import Image -from konabot.plugins.marchtoy.command import Scene +from konabot.plugins.marchtoy.scene import Scene async def render(command: str, res: tuple[int, int]): fs = Scene(command).compile() diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 366d383..364c1dd 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -1,6 +1,8 @@ import numpy as np from konabot.plugins.marchtoy.texture import Texture from konabot.plugins.marchtoy.utilities import ArgParser, Formatter +from nonebot import logger +from typing import Optional OBJECT_ENTRIES = {} @@ -80,7 +82,7 @@ class Object: self.transform: Transform = Transform() self.texture: Texture = Texture() - def parse_args(self, args: list[str]): + def parse_args(self, args: str): raise NotImplementedError def get_transformed(self, p: np.ndarray) -> np.ndarray: @@ -94,9 +96,6 @@ class Object: def sdf_block_glsl(self) -> str: raise NotImplementedError - def sdf(self, _p: np.ndarray) -> float: - return NotImplementedError - @make_obj("cube", "box") class Cube(Object): @@ -104,34 +103,24 @@ class Cube(Object): super().__init__() self.size = _size - def parse_args(self, args: list[str]): - self.size = ArgParser.as_vec3(args) + def parse_args(self, args: str): + self.size = ArgParser.as_vec3(ArgParser.to_params(args)) def sdf_block_glsl(self) -> str: return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))" - def sdf(self, _p): - p = self.get_transformed(_p) - p = np.abs(p) - self.size - return np.linalg.norm(np.maximum(p, 0.0)) + np.minimum(np.max(p), 0.0) - - @make_obj("sphere", "ball") class Sphere(Object): def __init__(self, _radius: float = 1.0) -> None: super().__init__() self.radius = _radius - def parse_args(self, args: list[str]): - self.radius = ArgParser.as_float(args) + def parse_args(self, args: str): + self.radius = ArgParser.as_float(ArgParser.to_params(args)) def sdf_block_glsl(self) -> str: return f"sdSphere({self.transform.p_expr()}, {self.radius})" - def sdf(self, _p): - p = self.get_transformed(_p) - return np.linalg.norm(p) - self.radius - @make_obj("cylinder", "cyl") class Cylinder(Object): @@ -140,8 +129,8 @@ class Cylinder(Object): self.radius = _radius self.height = _height - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.radius = param[0] self.height = param[1] @@ -150,16 +139,6 @@ class Cylinder(Object): f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})" ) - def sdf(self, _p): - p = self.get_transformed(_p) - d = np.abs(np.array([np.linalg.norm(p[[0, 2]]), p[1]])) - np.array( - [self.radius, self.height] - ) - return np.minimum(np.maximum(d[0], d[1]), 0.0) + np.linalg.norm( - np.maximum(d, 0.0) - ) - - @make_obj("torus") class Torus(Object): def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None: @@ -167,19 +146,14 @@ class Torus(Object): self.r1 = _r1 self.r2 = _r2 - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.r1 = param[0] self.r2 = param[1] def sdf_block_glsl(self) -> str: return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))" - def sdf(self, _p): - p = self.get_transformed(_p) - q = np.array([np.linalg.norm(p[[0, 2]]) - self.r1, p[1]]) - return np.linalg.norm(q) - self.r2 - @make_obj("capsule", "pill") class Capsule(Object): @@ -188,19 +162,32 @@ class Capsule(Object): self.h = _h self.r = _r - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.h = param[0] self.r = param[1] def sdf_block_glsl(self) -> str: return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})" - def sdf(self, _p): - p = self.get_transformed(_p) - p[1] -= np.clip(p[1], 0.0, self.h) - return np.linalg.norm(p) - self.r +@make_obj("smoothed", "mix") +class Smoothed(Object): + def __init__(self, _a = None, _b = None, _k: float = 0.25): + super().__init__() + self.a_index: int = _a + self.b: Optional[Object] = _b + self.k: float = _k + + def parse_args(self, args: str): + # from konabot.plugins.marchtoy.command import CommandChainParser + try: + raise Exception + except Exception as e: + raise Exception(f"cannot build smoothed object over {args}: {e}") + + def sdf_block_glsl(self): + return f"smin({self.a.sdf_block_glsl()}, {self.b.sdf_block_glsl()}, {self.k})" @make_obj("camera", "cam") class Camera(Object): @@ -209,5 +196,6 @@ class Camera(Object): self.focus = _focus self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0) - def parse_args(self, args: list[str]): - self.focus = ArgParser.as_float(args) + def parse_args(self, args: str): + self.focus = ArgParser.as_float(ArgParser.to_params(args)) + diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py new file mode 100644 index 0000000..289d88c --- /dev/null +++ b/konabot/plugins/marchtoy/scene.py @@ -0,0 +1,88 @@ +from typing import Optional +import pathlib +import numpy as np +from nonebot import logger + +class Scene: + def __init__(self, _instruction: str) -> None: + from konabot.plugins.marchtoy.command import CommandChainParser, CommandParser + from konabot.plugins.marchtoy.op import OPERATION_ENTRIES + from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES + logger.info(f"building scene: {_instruction}") + 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}.\n{e}" + ) 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_glsl() + 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 vec4(" + 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 diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index a7b7fab..8cc4068 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -33,7 +33,12 @@ float sdTorus( vec3 p, vec2 t ) vec2 q = vec2(length(p.xz)-t.x,p.y); return length(q)-t.y; } - +float smin( float a, float b, float k ) +{ + k *= 1.0; + float r = exp2(-a/k) + exp2(-b/k); + return -k*log2(r); +} sdQuery sd(vec3 p) { sdQuery qry; qry.value = 100000000.0; diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index 34d18f7..dcadc52 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -26,6 +26,17 @@ class Formatter: TODO: 除零出现 nan 情况的单独处理 """ class ArgParser: + @staticmethod + def to_params(args: str, delim: str = ',') -> list[str]: + _params = args.replace(" ", "").split(delim) + params: list[str] = [] + # while 还是太过于令人生畏了 + for param in _params: + if param == "": + continue + params.append(param) + return paramss + @staticmethod def as_float(args: list[str], default: float = 0.0) -> float: try: