Merge pull request '新增 marchtoy' (#70) from bkbkzzzz/konabot:marchtoy_gl into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #70
Reviewed-by: 钟晓帕 <Passthem183@gmail.com>
This commit is contained in:
2026-04-27 02:42:16 +08:00
17 changed files with 2024 additions and 1066 deletions

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ __pycache__
/.venv
/venv
*.egg-info
# OS 相关
.DS_Store

View File

@ -0,0 +1,22 @@
# 指令介绍
简易 Raymarch 小玩具
用法march <scene>
march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(0.5).pos(-5).lookat(0)
# 主要语法
<scene> ::= <scene> "." <op>
| <obj>
<obj> ::= <obj_ty>
| <obj_ty> "(" <args> ")"
<op> ::= <op_ty>
| <op_ty> "(" <args> ")"
<args> ::= <args> "," <arg>
| <arg>
其中 obj_ty、op_ty 分别为物体类型(如 cube、sphere、torus 等)与变换类型(如 pos、rot
# 特殊说明
<op_ty> 不包含 scale因为本工具基于 SDF 渲染,非正交的变换会破坏 SDF 的性质。

View File

@ -0,0 +1,19 @@
from nonebot import on_command
from nonebot.adapters import Message
from nonebot_plugin_alconna import UniMessage
from nonebot.params import CommandArg
import konabot.plugins.marchtoy.gl_render as render
import io
cmd_marchtoy = on_command("march")
@cmd_marchtoy.handle()
async def _(args: Message = CommandArg()):
if cmd := args.extract_plain_text():
try:
img = await render.render(cmd, (512, 512))
buffer = io.BytesIO()
# img.show()
img.save(buffer, format="PNG")
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())

View File

@ -0,0 +1,148 @@
"""
raymarch toy
usage: march <scene1> <scene2> ... <sceneN>
<scene> ::= <scene> "." <command>
| <command>
<command> ::= <id>
| <id> "(" <args> ")"
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>", 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

@ -0,0 +1,35 @@
"""
headless moderngl
"""
import pathlib
import moderngl
import numpy as np
from PIL import Image
from konabot.plugins.marchtoy.command import Scene
async def render(command: str, res: tuple[int, int]):
fs = Scene(command).compile()
PATH = pathlib.Path(__file__).parent / "shaders"
with (PATH / "vert.glsl").open(encoding='utf-8') as f:
vs = f.read()
ctx = moderngl.create_context(standalone=True)
ctx.gc_mode = "auto"
try:
program = ctx.program(
vertex_shader=vs,
fragment_shader=fs
)
except Exception as e:
raise Exception(f"cannot compile glsl: {e}") from e
uniform = program['u_resolution']
uniform.write(np.array(res, dtype=np.float32))
vertices = np.array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0], dtype=np.float32)
indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.int32)
ibo = ctx.buffer(indices)
vbo = ctx.buffer(vertices)
vao = ctx.vertex_array(program, vbo, 'in_position', index_buffer = ibo)
fbo = ctx.simple_framebuffer(res)
fbo.use()
fbo.clear(0.0, 0.0, 0.0, 0.0)
vao.render(moderngl.TRIANGLES)
return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1)

View File

@ -0,0 +1,174 @@
import numpy as np
from konabot.plugins.marchtoy.texture import Texture
from konabot.plugins.marchtoy.utilities import ArgParser, Formatter
OBJECT_ENTRIES = {}
def make_obj(*name: str):
def decorator(cls):
# OBJECT_ENTRIES[name] = cls
for alias in [*name]:
OBJECT_ENTRIES[alias] = cls
return cls
return decorator
class Transform:
def __init__(self) -> None:
self.t: np.ndarray = np.identity(4, dtype=np.float32)
@staticmethod
def normalize(p: np.ndarray) -> np.ndarray:
return p / (np.sqrt(np.dot(p, p)) + 1e-8)
def translate(self, x: float, y: float, z: float):
mat = np.identity(4, dtype=np.float32)
mat[0:3, 3] = [x, y, z]
self.t = mat @ self.t
return self
# scale 会破坏 sdf 的性质 梯度大小会变 导致 overshoot 等问题
def scale(self, x: float, y: float, z: float):
mat = np.identity(4, dtype=np.float32)
mat[0, 0], mat[1, 1], mat[2, 2] = x, y, z
self.t = mat @ self.t
return self
def rotate(self, x: float, y: float, z: float):
cx, sx = np.cos(x), np.sin(x)
cy, sy = np.cos(y), np.sin(y)
cz, sz = np.cos(z), np.sin(z)
mat = np.identity(4, dtype=np.float32)
mat[0, 0] = cy * cz
mat[0, 1] = sx * sy * cz - cx * sz
mat[0, 2] = cx * sy * cz + sx * sz
mat[1, 0] = cy * sz
mat[1, 1] = sx * sy * sz + cx * cz
mat[1, 2] = cx * sy * sz - sx * cz
mat[2, 0] = -sy
mat[2, 1] = sx * cy
mat[2, 2] = cx * cy
self.t = mat @ self.t
return self
def lookat(
self, x: float, y: float, z: float, up: np.ndarray = np.array([0.0, 1.0, 0.0])
):
p = self.t[0:3, 3]
q = np.array([x, y, z])
zaxis = Transform.normalize(q - p)
xaxis = Transform.normalize(np.cross(zaxis, up))
yaxis = Transform.normalize(np.cross(xaxis, zaxis))
self.t[:3, 0] = xaxis
self.t[:3, 1] = yaxis
self.t[:3, 2] = -zaxis
return self
def p_expr(self) -> str:
inv = np.linalg.inv(self.t) # + 1e-5 * np.identity(4, dtype=np.float32))
return f"({Formatter.float4(inv)} * vec4(p, 1.0)).xyz"
class Object:
def __init__(self) -> None:
self.transform: Transform = Transform()
self.texture: Texture = Texture()
def parse_args(self, args: list[str]):
raise NotImplementedError
def sdf_block(self) -> str:
raise NotImplementedError
@make_obj("cube", "box")
class Cube(Object):
def __init__(self, _size: np.ndarray = np.array([1.0, 1.0, 1.0])) -> None:
super().__init__()
self.size = _size
def parse_args(self, args: list[str]):
self.size = ArgParser.as_vec3(args)
def sdf_block(self) -> str:
return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))"
@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 sdf_block(self) -> str:
return f"sdSphere({self.transform.p_expr()}, {self.radius})"
@make_obj("cylinder", "cyl")
class Cylinder(Object):
def __init__(self, _radius: float = 1.0, _height: float = 1.0) -> None:
super().__init__()
self.radius = _radius
self.height = _height
def parse_args(self, args: list[str]):
param = ArgParser.as_vec2(args)
self.radius = param[0]
self.height = param[1]
def sdf_block(self) -> str:
return (
f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})"
)
@make_obj("torus")
class Torus(Object):
def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None:
super().__init__()
self.r1 = _r1
self.r2 = _r2
def parse_args(self, args: list[str]):
param = ArgParser.as_vec2(args)
self.r1 = param[0]
self.r2 = param[1]
def sdf_block(self) -> str:
return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))"
@make_obj("capsule", "pill")
class Capsule(Object):
def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None:
super().__init__()
self._h = _h
self._r = _r
def parse_args(self, args: list[str]):
param = ArgParser.as_vec2(args)
self._h = param[0]
self._r = param[1]
def sdf_block(self) -> str:
return f"sdVerticalCapsule({self.transform.p_expr()}, {self._h}, {self._r})"
@make_obj("camera", "cam")
class Camera(Object):
def __init__(self, _focus: float = 1.0) -> None:
super().__init__()
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)

View File

@ -0,0 +1,50 @@
from konabot.plugins.marchtoy.obj import Object
from konabot.plugins.marchtoy.utilities import ArgParser
OPERATION_ENTRIES = {}
def make_operation(*name: str):
def decorator(op):
# OPERATION_ENTRIES[name] = op
for alias in [*name]:
OPERATION_ENTRIES[alias] = op
return op
return decorator
@make_operation("pos", "translate", "position", "p")
def translate(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args)
obj.transform.translate(pos[0], pos[1], pos[2])
@make_operation("rot", "rotate", "r")
def rotate(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args)
obj.transform.rotate(pos[0], pos[1], pos[2])
@make_operation("lookat", "look", "l")
def lookat(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args)
obj.transform.lookat(pos[0], pos[1], pos[2])
@make_operation("color", "col", "texture")
def color(obj: Object, args: list[str]):
try:
if len(args) == 1:
col = ArgParser.as_literal_color(args)
obj.texture.color = (col[0], col[1], col[2], col[3])
elif len(args) == 3:
col = ArgParser.as_vec3(args)
obj.texture.color = (col[0], col[1], col[2], 1.0)
elif len(args) == 4:
col = ArgParser.as_vec4(args)
obj.texture.color = (col[0], col[1], col[2], col[3])
else:
raise Exception("unknown color")
except:
raise Exception("unknown color")

View File

@ -0,0 +1,99 @@
#version 330
// compatibility
#define float4x4 mat4x4
#define float4 vec4
#define float3 vec3
#define float2 vec2
const float EPS = 0.001;
const int MAX_ITER = 128;
uniform vec2 u_resolution;
out vec4 fragColor;
struct sdQuery {
float value;
int obj_id;
};
float sdCube(vec3 p, vec3 b) {
p = abs(p) - b;
return length(max(p, 0.0)) + min(max(p.x, max(p.y, p.z)), 0.0);
}
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdCappedCylinder( vec3 p, float r, float h )
{
vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}
float sdVerticalCapsule( vec3 p, float h, float r )
{
p.y -= clamp( p.y, 0.0, h );
return length( p ) - r;
}
float sdTorus( vec3 p, vec2 t )
{
vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}
sdQuery sd(vec3 p) {
sdQuery qry;
qry.value = 100000000.0;
qry.obj_id = -1;
<SDF_BLOCK>
return qry;
}
vec3 nrm(vec3 p) {
vec2 d = vec2(EPS, 0.0);
return normalize(vec3(
sd(p + d.xyy).value - sd(p - d.xyy).value,
sd(p + d.yxy).value - sd(p - d.yxy).value,
sd(p + d.yyx).value - sd(p - d.yyx).value
));
}
vec4 materialColor(int obj_id) {
<COLOR_BLOCK>
return vec4(1.0);
}
vec4 color(vec3 p, int obj_id) {
vec3 normal = nrm(p);
vec3 light_dir = normalize(vec3(0.5, 0.8, -0.6));
float light = 0.2 + 0.8 * max(dot(normal, light_dir), 0.0);
vec4 base = materialColor(obj_id);
return vec4(base.rgb * light, base.a);
}
vec4 march(vec3 p, vec3 r) {
sdQuery qry;
vec4 col = vec4(0.0);
for(int i = 0; i < MAX_ITER; ++i) {
qry = sd(p);
if(qry.value < EPS){
col = color(p, qry.obj_id);
break;
}
p += qry.value * r;
}
return col;
}
void main() {
vec2 uv = 2. * (gl_FragCoord.xy / u_resolution - .5) * vec2(u_resolution.x / u_resolution.y, 1.);
vec3 c_p = vec3(<CAM_POS>);
vec3 c_z = normalize(vec3(<CAM_DIR>));
vec3 world_up = abs(c_z.y) > 0.999 ? vec3(0., 0., 1.) : vec3(0., 1., 0.);
vec3 c_x = normalize(cross(c_z, world_up));
vec3 c_y = normalize(cross(c_x, c_z));
mat3 view = mat3(c_x, c_y, c_z);
vec3 r = normalize(vec3(uv, <CAM_FOCUS>));
fragColor = march(c_p, view * r);
}

View File

@ -0,0 +1,6 @@
#version 330
in vec2 in_position;
void main() {
gl_Position = vec4(in_position, 0.0, 1.0);
}

View File

@ -0,0 +1,42 @@
raise DeprecationWarning
from command import Scene
import skia
import struct
from PIL import Image
import numpy as np
# 暂时先照抄小帕的了,之后有空再单独封装一下
async def render(cmd: str, width: int, height: int):
scene = Scene(cmd)
surface = skia.Surface(width, height)
sksl_code = scene.compile()
runtime_effect = skia.RuntimeEffect.MakeForShader(scene.compile())
if runtime_effect is None:
raise Exception("cannot compile sksl shader")
uv_uniform = struct.pack("ff", float(width), float(height))
uniform_data = skia.Data.MakeWithCopy(uv_uniform)
shader = runtime_effect.makeShader(uniform_data, None, 0)
canvas = surface.getCanvas()
canvas.clear(skia.Color(0, 0, 0, 0))
paint = skia.Paint()
paint.setShader(shader)
canvas.drawRect(skia.Rect.MakeWH(width, height), paint)
image = surface.makeImageSnapshot()
target_info = skia.ImageInfo.Make(
image.width(),
image.height(),
skia.ColorType.kBGRA_8888_ColorType,
skia.AlphaType.kPremul_AlphaType,
)
pixel_data = bytearray(
image.width() * image.height() * 4
) # 4 bytes per pixel (BGRA)
success = image.readPixels(target_info, pixel_data, target_info.minRowBytes())
img_array = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4))
rgb_array = img_array[:, :, [2, 1, 0]]
pil_img = Image.fromarray(rgb_array)
return pil_img

View File

@ -0,0 +1,54 @@
from dataclasses import dataclass
COLORS = {
"red": (1.0, 0.0, 0.0, 1.0),
"green": (0.0, 1.0, 0.0, 1.0),
"blue": (0.0, 0.0, 1.0, 1.0),
"white": (1.0, 1.0, 1.0, 1.0),
"black": (0.0, 0.0, 0.0, 1.0),
"gray": (0.5, 0.5, 0.5, 1.0),
"light_gray": (0.75, 0.75, 0.75, 1.0),
"dark_gray": (0.25, 0.25, 0.25, 1.0),
"dark_red": (0.5, 0.0, 0.0, 1.0),
"crimson": (0.86, 0.08, 0.24, 1.0),
"pink": (1.0, 0.75, 0.8, 1.0),
"hot_pink": (1.0, 0.41, 0.71, 1.0),
"orange_red": (1.0, 0.27, 0.0, 1.0),
"orange": (1.0, 0.65, 0.0, 1.0),
"gold": (1.0, 0.84, 0.0, 1.0),
"yellow": (1.0, 1.0, 0.0, 1.0),
"light_yellow": (1.0, 1.0, 0.88, 1.0),
"khaki": (0.94, 0.90, 0.55, 1.0),
"light_green": (0.56, 0.93, 0.56, 1.0),
"lime": (0.0, 1.0, 0.0, 1.0),
"forest_green": (0.13, 0.55, 0.13, 1.0),
"dark_green": (0.0, 0.39, 0.0, 1.0),
"olive": (0.5, 0.5, 0.0, 1.0),
"teal": (0.0, 0.5, 0.5, 1.0),
"light_blue": (0.68, 0.85, 0.9, 1.0),
"sky_blue": (0.53, 0.81, 0.92, 1.0),
"cyan": (0.0, 1.0, 1.0, 1.0),
"navy": (0.0, 0.0, 0.5, 1.0),
"royal_blue": (0.25, 0.41, 0.88, 1.0),
"steel_blue": (0.27, 0.51, 0.71, 1.0),
"purple": (0.5, 0.0, 0.5, 1.0),
"magenta": (1.0, 0.0, 1.0, 1.0),
"violet": (0.93, 0.51, 0.93, 1.0),
"lavender": (0.90, 0.90, 0.98, 1.0),
"indigo": (0.29, 0.0, 0.51, 1.0),
"brown": (0.65, 0.16, 0.16, 1.0),
"saddle_brown": (0.55, 0.27, 0.07, 1.0),
"chocolate": (0.82, 0.41, 0.12, 1.0),
"tan": (0.82, 0.71, 0.55, 1.0),
"beige": (0.96, 0.96, 0.86, 1.0),
"coral": (1.0, 0.5, 0.31, 1.0),
"salmon": (0.98, 0.5, 0.45, 1.0),
"turquoise": (0.25, 0.88, 0.82, 1.0),
"aqua": (0.0, 1.0, 1.0, 1.0),
"plum": (0.87, 0.63, 0.87, 1.0),
"wheat": (0.96, 0.87, 0.70, 1.0),
}
@dataclass
class Texture:
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)

View File

@ -0,0 +1,94 @@
import numpy as np
from konabot.plugins.marchtoy.texture import COLORS
class Formatter:
@staticmethod
def float4(m: np.ndarray) -> str:
if m.shape != (4, 4):
m = np.identity(4)
v_0 = ", ".join([str(x) for x in m[:, 0]])
v_1 = ", ".join([str(x) for x in m[:, 1]])
v_2 = ", ".join([str(x) for x in m[:, 2]])
v_3 = ", ".join([str(x) for x in m[:, 3]])
return f"float4x4(float4({v_0}), float4({v_1}), float4({v_2}), float4({v_3}))"
@staticmethod
def vec4(m: np.ndarray) -> str:
if m.shape != (4, 4):
m = np.identity(4)
v_0 = ", ".join([str(x) for x in m[:, 0]])
v_1 = ", ".join([str(x) for x in m[:, 1]])
v_2 = ", ".join([str(x) for x in m[:, 2]])
v_3 = ", ".join([str(x) for x in m[:, 3]])
return f"mat4(vec4({v_0}), vec4({v_1}), vec4({v_2}), vec4({v_3}))"
"""
TODO: 除零出现 nan 情况的单独处理
"""
class ArgParser:
@staticmethod
def as_float(args: list[str], default: float = 0.0) -> float:
try:
if len(args) >= 1:
x = float(args[0])
return x
except:
raise Exception(f"cannot parse {args}")
return default
@staticmethod
def as_vec2(
args: list[str], default: np.ndarray = np.array((0.0, 0.0))
) -> np.ndarray:
try:
if len(args) == 1:
x = float(args[0])
return np.array((x, x))
elif len(args) == 2:
x = float(args[0])
y = float(args[1])
return np.array((x, y))
except:
raise Exception(f"cannot parse {args}")
return default
@staticmethod
def as_vec3(
args: list[str], default: np.ndarray = np.array((0.0, 0.0, 0.0))
) -> np.ndarray:
try:
if len(args) == 1:
x = float(args[0])
return np.array((x, x, x))
elif len(args) == 3:
x = float(args[0])
y = float(args[1])
z = float(args[2])
return np.array((x, y, z))
except:
raise Exception(f"cannot parse {args}")
return default
@staticmethod
def as_vec4(
args: list[str], default: np.ndarray = np.array((0.0, 0.0, 0.0, 0.0))
) -> np.ndarray:
try:
if len(args) == 1:
x = float(args[0])
return np.array((x, x, x, x))
elif len(args) == 4:
x = float(args[0])
y = float(args[1])
z = float(args[2])
w = float(args[3])
return np.array((x, y, z, w))
except:
raise Exception(f"cannot parse {args}")
return default
@staticmethod
def as_literal_color(args: list[str], default=np.array((1.0, 1.0, 1.0, 1.0))):
if len(args) == 1 and args[0] in COLORS:
return np.array(COLORS[args[0]])
return default

View File

@ -3,15 +3,17 @@ from nonebot_plugin_alconna import Alconna, Args, Option, UniMessage, on_alconna
from konabot.common.nb.exc import BotExceptionMessage
from konabot.plugins.sksl.run_sksl import render_sksl_shader_to_gif
cmd_run_sksl = on_alconna(
Alconna(
"shadertool",
Option("--width", Args["width_", int]),
Option("--height", Args["height_", int]),
Option("--duration", Args["duration_", float]),
Option("--fps", Args["fps_", float]),
Args["code", str],
)
)
cmd_run_sksl = on_alconna(Alconna(
"shadertool",
Option("--width", Args["width_", int]),
Option("--height", Args["height_", int]),
Option("--duration", Args["duration_", float]),
Option("--fps", Args["fps_", float]),
Args["code", str],
))
@cmd_run_sksl.handle()
async def _(
@ -34,10 +36,12 @@ async def _(
if width_ > 640 or height_ > 640:
raise BotExceptionMessage("最大支持 640x640 啦!不要太大啦!")
code = code.strip("\"").strip("'")
code = code.strip('"').strip("'")
try:
res = await render_sksl_shader_to_gif(code, width_, height_, duration_, fps_)
await cmd_run_sksl.send(await UniMessage().image(raw=res).export())
except (ValueError, RuntimeError) as e:
await cmd_run_sksl.send(await UniMessage().text(f"渲染时遇到了问题:\n\n{e}").export())
await cmd_run_sksl.send(
await UniMessage().text(f"渲染时遇到了问题:\n\n{e}").export()
)

View File

@ -24,7 +24,6 @@ def _pack_uniforms(uniforms_dict, width, height, time_val):
# 移除填充字节,使用紧凑布局
return time_bytes + res_bytes
def _render_sksl_shader_to_gif(
sksl_code: str,
width: int = 256,
@ -152,4 +151,4 @@ async def render_sksl_shader_to_gif(
height,
duration,
fps,
)
)

2297
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@ dependencies = [
"imagetext-py (>=2.2.0,<3.0.0)",
"opencv-python-headless (>=4.12.0.88,<5.0.0.0)",
"returns (>=0.26.0,<0.27.0)",
"skia-python (>=138.0,<139.0)",
"nonebot-plugin-analysis-bilibili (>=2.8.1,<3.0.0)",
"qrcode (>=8.2,<9.0)",
"nanoid (>=2.0.0,<3.0.0)",
@ -39,6 +38,8 @@ dependencies = [
"pytest-cov (>=7.0.0,<8.0.0)",
"aiosignal (>=1.4.0,<2.0.0)",
"pytest-mock (>=3.15.1,<4.0.0)",
"skia-python (>=144.0.post2,<145.0)",
"moderngl (>=5.12.0,<6.0.0)",
]
[tool.poetry]

View File

@ -0,0 +1,17 @@
import sys
from pathlib import Path
PLUGIN_DIR = Path(__file__).resolve().parents[1] / "konabot" / "plugins" / "marchtoy"
if str(PLUGIN_DIR) not in sys.path:
sys.path.insert(0, str(PLUGIN_DIR))
from obj import Transform
def test_translate_expression_puts_offset_in_matrix_column():
transform = Transform().translate(1.0, 2.0, 3.0)
expr = transform.p_expr()
assert "float4(-1.0, -2.0, -3.0, 1.0)" in expr