Compare commits

...

2 Commits

Author SHA1 Message Date
88f1f45b94 manual 补充; cpu 写法速度不可接受 2026-04-27 15:28:13 +08:00
b7f90b0c9e manual 补充,尝试迁移 cpu backend 2026-04-27 14:57:32 +08:00
9 changed files with 88 additions and 59 deletions

View File

@ -1,22 +1,36 @@
# 指令介绍
简易 Raymarch 小玩具
用法march <scene>
用法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>
`<scene>` ::= `<scene>` "." `<op>` |`<obj>`
<obj> ::= <obj_ty>
| <obj_ty> "(" <args> ")"
`<obj>` ::= `<obj_ty>` | `<obj_ty>` "(" <args> ")"
<op> ::= <op_ty>
| <op_ty> "(" <args> ")"
`<op>` ::= `<op_ty>` | `<op_ty>` "(" `<args>` ")"
<args> ::= <args> "," <arg>
| <arg>
`<args>` ::= `<args>` "," `<arg>` | `<arg>`
其中 obj_tyop_ty 分别为物体类型(如 cubespheretorus 等)与变换类型(如 posrot
其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。
# 支持的物体
目前支持的物体有(不包含 alias
`cube`:可选参数长宽高
`sphere`:可选参数半径
`torus`:可选参数半径与粗细
`cylinder`:可选参数半径与高度
`capsule`:可选参数高度与半径
特殊物体:
`camera`:可选参数焦距
# 支持的变换
目前支持的变换有
`pos`
`rot`
`color`
`lookat`
# 特殊说明
<op_ty> 不包含 scale,因为本工具基于 SDF 渲染,非正交的变换会破坏 SDF 的性质。
`<op_ty>` 不包含 scale非正交的变换会破坏 SDF 的性质。

View File

@ -3,6 +3,7 @@ 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 konabot.plugins.marchtoy.cpu_render as render
import io
cmd_marchtoy = on_command("march")
@cmd_marchtoy.handle()

View File

@ -19,9 +19,9 @@ 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
@ -103,7 +103,7 @@ class Scene:
raise Exception(f"{op_id} is not a valid operation.")
try:
sdf_block = obj_instance.sdf_block()
sdf_block = obj_instance.sdf_block_glsl()
self.canvas_objs.append((obj_instance, sdf_block))
except:
if type(obj_instance) == Camera:
@ -127,7 +127,7 @@ class Scene:
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"if(obj_id == {index}) return vec4("
f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n"
)
index += 1

View File

@ -12,7 +12,7 @@ async def render(command: str, res: tuple[int, int]):
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 = moderngl.create_context(standalone=True, backend='egl')
ctx.gc_mode = "auto"
try:
program = ctx.program(
@ -32,4 +32,5 @@ async def render(command: str, res: tuple[int, int]):
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)
return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1)

View File

@ -72,7 +72,7 @@ class Transform:
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"
return f"({Formatter.vec4(inv)} * vec4(p, 1.0)).xyz"
class Object:
@ -83,9 +83,20 @@ class Object:
def parse_args(self, args: list[str]):
raise NotImplementedError
def sdf_block(self) -> str:
def get_transformed(self, p: np.ndarray) -> np.ndarray:
if p.shape != (3,):
raise Exception(f"{p} is not a vec3")
p = p.copy()
inv = np.linalg.inv(self.transform.t)
return (inv @ np.array((*p, 1)))[:3]
def sdf_block_glsl(self) -> str:
raise NotImplementedError
def sdf(self, _p: np.ndarray) -> float:
return NotImplementedError
@make_obj("cube", "box")
class Cube(Object):
@ -96,9 +107,14 @@ class Cube(Object):
def parse_args(self, args: list[str]):
self.size = ArgParser.as_vec3(args)
def sdf_block(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]}))"
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):
@ -109,9 +125,13 @@ class Sphere(Object):
def parse_args(self, args: list[str]):
self.radius = ArgParser.as_float(args)
def sdf_block(self) -> str:
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):
@ -125,11 +145,20 @@ class Cylinder(Object):
self.radius = param[0]
self.height = param[1]
def sdf_block(self) -> str:
def sdf_block_glsl(self) -> str:
return (
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):
@ -143,24 +172,34 @@ class Torus(Object):
self.r1 = param[0]
self.r2 = param[1]
def sdf_block(self) -> str:
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):
def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None:
super().__init__()
self._h = _h
self._r = _r
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]
self.h = param[0]
self.r = param[1]
def sdf_block(self) -> str:
return f"sdVerticalCapsule({self.transform.p_expr()}, {self._h}, {self._r})"
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("camera", "cam")

View File

@ -1,16 +1,8 @@
#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;

View File

@ -1,4 +1,3 @@
raise DeprecationWarning
from command import Scene
import skia
import struct
@ -8,12 +7,12 @@ import numpy as np
# 暂时先照抄小帕的了,之后有空再单独封装一下
async def render(cmd: str, width: int, height: int):
scene = Scene(cmd)
async def render(instruction: str, res: tuple[int, int]):
scene = Scene(instruction)
width, height = res
surface = skia.Surface(width, height)
sksl_code = scene.compile()
runtime_effect = skia.RuntimeEffect.MakeForShader(scene.compile())
runtime_effect = skia.RuntimeEffect.MakeForShader(sksl_code)
if runtime_effect is None:
raise Exception("cannot compile sksl shader")

View File

@ -3,7 +3,7 @@ from konabot.plugins.marchtoy.texture import COLORS
class Formatter:
@staticmethod
def float4(m: np.ndarray) -> str:
def float4x4(m: np.ndarray) -> str:
if m.shape != (4, 4):
m = np.identity(4)
v_0 = ", ".join([str(x) for x in m[:, 0]])

View File

@ -1,17 +0,0 @@
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