regex fix, more primitives

This commit is contained in:
bk_office
2026-04-27 18:11:32 +08:00
parent 88f1f45b94
commit d80d8d91c2
7 changed files with 145 additions and 139 deletions

View File

@ -17,4 +17,4 @@ async def _(args: Message = CommandArg()):
buffer.seek(0) buffer.seek(0)
await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export())
except Exception as e: except Exception as e:
await cmd_marchtoy.send(await UniMessage.text(f"发生了错误: {e}").export()) await cmd_marchtoy.send(await UniMessage.text(f"cannot render: {e}").export())

View File

@ -13,15 +13,9 @@ example:
march cube.pos(0, 0, 0).color(red) cam(1.0).pos(1, 1, 1).lookat(0, 0, 0) 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 from dataclasses import dataclass
import re import re
import numpy as np
from dataclasses import dataclass 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 @dataclass
class Command: class Command:
id: str id: str
@ -29,7 +23,7 @@ class Command:
class CommandChainParser: 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: def __init__(self, _command_chain: str) -> None:
self.command_chain = _command_chain self.command_chain = _command_chain
@ -46,7 +40,7 @@ class CommandChainParser:
class CommandParser: 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]+(?=\(|\.|$)" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)"
def __init__(self, _command: str) -> None: def __init__(self, _command: str) -> None:
@ -61,88 +55,8 @@ class CommandParser:
if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd): if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd):
cmd_id = cmd_id_qry[0] cmd_id = cmd_id_qry[0]
self.command = self.command[len(cmd) + 1 :] self.command = self.command[len(cmd) + 1 :]
cmd_args = cmd[len(cmd_id) + 1 : -1].replace(" ", "").split(",") cmd_args = cmd[len(cmd_id) + 1 : -1]#.replace(" ", "").split(",")
while "" in cmd_args: # while "" in cmd_args:
cmd_args.remove("") # cmd_args.remove("")
return Command(cmd_id, cmd_args) return Command(cmd_id, cmd_args)
raise StopIteration 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>", sdf_block)
content = content.replace("<COLOR_BLOCK>", 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(
"<CAM_POS>", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}"
)
content = content.replace("<CAM_FOCUS>", str(cam_focus))
content = content.replace(
"<CAM_DIR>", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}"
)
return content

View File

@ -5,7 +5,7 @@ import pathlib
import moderngl import moderngl
import numpy as np import numpy as np
from PIL import Image 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]): async def render(command: str, res: tuple[int, int]):
fs = Scene(command).compile() fs = Scene(command).compile()

View File

@ -1,6 +1,8 @@
import numpy as np import numpy as np
from konabot.plugins.marchtoy.texture import Texture from konabot.plugins.marchtoy.texture import Texture
from konabot.plugins.marchtoy.utilities import ArgParser, Formatter from konabot.plugins.marchtoy.utilities import ArgParser, Formatter
from nonebot import logger
from typing import Optional
OBJECT_ENTRIES = {} OBJECT_ENTRIES = {}
@ -80,7 +82,7 @@ class Object:
self.transform: Transform = Transform() self.transform: Transform = Transform()
self.texture: Texture = Texture() self.texture: Texture = Texture()
def parse_args(self, args: list[str]): def parse_args(self, args: str):
raise NotImplementedError raise NotImplementedError
def get_transformed(self, p: np.ndarray) -> np.ndarray: def get_transformed(self, p: np.ndarray) -> np.ndarray:
@ -94,9 +96,6 @@ class Object:
def sdf_block_glsl(self) -> str: def sdf_block_glsl(self) -> str:
raise NotImplementedError raise NotImplementedError
def sdf(self, _p: np.ndarray) -> float:
return NotImplementedError
@make_obj("cube", "box") @make_obj("cube", "box")
class Cube(Object): class Cube(Object):
@ -104,34 +103,24 @@ class Cube(Object):
super().__init__() super().__init__()
self.size = _size self.size = _size
def parse_args(self, args: list[str]): def parse_args(self, args: str):
self.size = ArgParser.as_vec3(args) self.size = ArgParser.as_vec3(ArgParser.to_params(args))
def sdf_block_glsl(self) -> str: def sdf_block_glsl(self) -> str:
return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))" 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") @make_obj("sphere", "ball")
class Sphere(Object): class Sphere(Object):
def __init__(self, _radius: float = 1.0) -> None: def __init__(self, _radius: float = 1.0) -> None:
super().__init__() super().__init__()
self.radius = _radius self.radius = _radius
def parse_args(self, args: list[str]): def parse_args(self, args: str):
self.radius = ArgParser.as_float(args) self.radius = ArgParser.as_float(ArgParser.to_params(args))
def sdf_block_glsl(self) -> str: def sdf_block_glsl(self) -> str:
return f"sdSphere({self.transform.p_expr()}, {self.radius})" 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") @make_obj("cylinder", "cyl")
class Cylinder(Object): class Cylinder(Object):
@ -140,8 +129,8 @@ class Cylinder(Object):
self.radius = _radius self.radius = _radius
self.height = _height self.height = _height
def parse_args(self, args: list[str]): def parse_args(self, args: str):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(ArgParser.to_params(args))
self.radius = param[0] self.radius = param[0]
self.height = param[1] self.height = param[1]
@ -150,16 +139,6 @@ class Cylinder(Object):
f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})" 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") @make_obj("torus")
class Torus(Object): class Torus(Object):
def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None: def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None:
@ -167,19 +146,14 @@ class Torus(Object):
self.r1 = _r1 self.r1 = _r1
self.r2 = _r2 self.r2 = _r2
def parse_args(self, args: list[str]): def parse_args(self, args: str):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(ArgParser.to_params(args))
self.r1 = param[0] self.r1 = param[0]
self.r2 = param[1] self.r2 = param[1]
def sdf_block_glsl(self) -> str: def sdf_block_glsl(self) -> str:
return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))" 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") @make_obj("capsule", "pill")
class Capsule(Object): class Capsule(Object):
@ -188,19 +162,32 @@ class Capsule(Object):
self.h = _h self.h = _h
self.r = _r self.r = _r
def parse_args(self, args: list[str]): def parse_args(self, args: str):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(ArgParser.to_params(args))
self.h = param[0] self.h = param[0]
self.r = param[1] self.r = param[1]
def sdf_block_glsl(self) -> str: def sdf_block_glsl(self) -> str:
return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})" return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})"
def sdf(self, _p): @make_obj("smoothed", "mix")
p = self.get_transformed(_p) class Smoothed(Object):
p[1] -= np.clip(p[1], 0.0, self.h) def __init__(self, _a = None, _b = None, _k: float = 0.25):
return np.linalg.norm(p) - self.r 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") @make_obj("camera", "cam")
class Camera(Object): class Camera(Object):
@ -209,5 +196,6 @@ class Camera(Object):
self.focus = _focus self.focus = _focus
self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0) self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0)
def parse_args(self, args: list[str]): def parse_args(self, args: str):
self.focus = ArgParser.as_float(args) self.focus = ArgParser.as_float(ArgParser.to_params(args))

View File

@ -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>", sdf_block)
content = content.replace("<COLOR_BLOCK>", 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(
"<CAM_POS>", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}"
)
content = content.replace("<CAM_FOCUS>", str(cam_focus))
content = content.replace(
"<CAM_DIR>", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}"
)
return content

View File

@ -33,7 +33,12 @@ float sdTorus( vec3 p, vec2 t )
vec2 q = vec2(length(p.xz)-t.x,p.y); vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.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 sd(vec3 p) {
sdQuery qry; sdQuery qry;
qry.value = 100000000.0; qry.value = 100000000.0;

View File

@ -26,6 +26,17 @@ class Formatter:
TODO: 除零出现 nan 情况的单独处理 TODO: 除零出现 nan 情况的单独处理
""" """
class ArgParser: 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 @staticmethod
def as_float(args: list[str], default: float = 0.0) -> float: def as_float(args: list[str], default: float = 0.0) -> float:
try: try: