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)
await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export())
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)
"""
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>", 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 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()

View File

@ -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))

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);
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;

View File

@ -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: