manual 补充,尝试迁移 cpu backend

This commit is contained in:
bk_office
2026-04-27 14:57:32 +08:00
parent b4f167e5f6
commit b7f90b0c9e
10 changed files with 179 additions and 59 deletions

View File

@ -1,22 +1,18 @@
# 指令介绍 # 指令介绍
简易 Raymarch 小玩具 简易 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) 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> `<scene>` ::= `<scene>` "." `<op>` |`<obj>`
| <obj>
<obj> ::= <obj_ty> `<obj>` ::= `<obj_ty>` | `<obj_ty>` "(" <args> ")"
| <obj_ty> "(" <args> ")"
<op> ::= <op_ty> `<op>` ::= `<op_ty>` | `<op_ty>` "(" `<args>` ")"
| <op_ty> "(" <args> ")"
<args> ::= <args> "," <arg> `<args>` ::= `<args>` "," `<arg>` | `<arg>`
| <arg>
其中 obj_tyop_ty 分别为物体类型(如 cubespheretorus 等)与变换类型(如 posrot 其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。
# 特殊说明 # 特殊说明
<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_plugin_alconna import UniMessage
from nonebot.params import CommandArg from nonebot.params import CommandArg
import konabot.plugins.marchtoy.gl_render as render import konabot.plugins.marchtoy.gl_render as render
# import konabot.plugins.marchtoy.cpu_render as render
import io import io
cmd_marchtoy = on_command("march") cmd_marchtoy = on_command("march")
@cmd_marchtoy.handle() @cmd_marchtoy.handle()

View File

@ -19,9 +19,9 @@ import re
import numpy as np import numpy as np
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES
from konabot.plugins.marchtoy.op import OPERATION_ENTRIES from konabot.plugins.marchtoy.op import OPERATION_ENTRIES
@dataclass @dataclass
class Command: class Command:
id: str id: str
@ -103,7 +103,7 @@ class Scene:
raise Exception(f"{op_id} is not a valid operation.") raise Exception(f"{op_id} is not a valid operation.")
try: try:
sdf_block = obj_instance.sdf_block() sdf_block = obj_instance.sdf_block_glsl()
self.canvas_objs.append((obj_instance, sdf_block)) self.canvas_objs.append((obj_instance, sdf_block))
except: except:
if type(obj_instance) == Camera: if type(obj_instance) == Camera:
@ -127,7 +127,7 @@ class Scene:
sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n"
color = obj.texture.color color = obj.texture.color
color_block += ( 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" f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n"
) )
index += 1 index += 1

View File

@ -0,0 +1,109 @@
from typing import Optional
import numpy as np
from PIL import Image
from konabot.plugins.marchtoy.command import Scene
from konabot.plugins.marchtoy.obj import Object, Transform
class SDQuery:
def __init__(self, _value: float = np.inf, _obj: Optional[Object] = None):
self.value: float = _value
self.obj: Optional[Object] = _obj
class Rasterizer:
EPS = 0.001
MAX_ITER = 128
MAX_DIST = 128.0
def __init__(self, _scene: Scene, _res: tuple[int, int]):
self.scene = _scene
self.width, self.height = _res
self.fb_rgba = np.zeros((self.height, self.width, 4), dtype=np.float32)
def sdf(self, p: np.ndarray) -> SDQuery:
qry = SDQuery()
for obj, _ in self.scene.canvas_objs:
sd = obj.sdf(p)
if sd < qry.value:
qry.value = sd
qry.obj = obj
return qry
def nrm(self, p: np.ndarray) -> np.ndarray:
dx = np.array((self.EPS, 0.0, 0.0))
dy = np.array((0.0, self.EPS, 0.0))
dz = np.array((0.0, 0.0, self.EPS))
grad = np.array(
[
self.sdf(p + dx).value - self.sdf(p - dx).value,
self.sdf(p + dy).value - self.sdf(p - dy).value,
self.sdf(p + dz).value - self.sdf(p - dz).value,
],
dtype=np.float32,
)
return Transform.normalize(grad)
def color(self, p: np.ndarray, obj: Object) -> np.ndarray:
normal = self.nrm(p)
light_dir = Transform.normalize(np.array((0.5, 0.8, -0.6), dtype=np.float32))
light = 0.2 + 0.8 * max(float(np.dot(normal, light_dir)), 0.0)
base = np.array(obj.texture.color, dtype=np.float32)
return np.array((base[0] * light, base[1] * light, base[2] * light, base[3]))
def march(self, origin: np.ndarray, direction: np.ndarray) -> np.ndarray:
p = origin.copy()
travel = 0.0
for _ in range(self.MAX_ITER):
qry = self.sdf(p)
if not np.isfinite(qry.value):
break
if qry.value < self.EPS and qry.obj is not None:
return self.color(p, qry.obj)
if qry.value > self.MAX_DIST or travel > self.MAX_DIST:
break
p += direction * qry.value
travel += qry.value
return np.zeros(4, dtype=np.float32)
def camera_basis(self) -> tuple[np.ndarray, np.ndarray]:
cam = self.scene.camera
cam_pos = cam.transform.t[0:3, 3]
cam_z = Transform.normalize(
(cam.transform.t @ np.array((0.0, 0.0, -1.0, 0.0), dtype=np.float32))[:3]
)
world_up = (
np.array((0.0, 0.0, 1.0), dtype=np.float32)
if abs(cam_z[1]) > 0.999
else np.array((0.0, 1.0, 0.0), dtype=np.float32)
)
cam_x = Transform.normalize(np.cross(cam_z, world_up))
cam_y = Transform.normalize(np.cross(cam_x, cam_z))
view = np.column_stack((cam_x, cam_y, cam_z))
return cam_pos, view
def rasterize(self) -> np.ndarray:
cam_pos, view = self.camera_basis()
resolution = np.array((self.width, self.height), dtype=np.float32)
aspect = np.array((self.width / self.height, 1.0), dtype=np.float32)
cam_focus = float(self.scene.camera.focus)
for y, x in np.ndindex(self.height, self.width):
frag_coord = np.array((x + 0.5, y + 0.5), dtype=np.float32)
uv = 2.0 * (frag_coord / resolution - 0.5) * aspect
ray_local = Transform.normalize(np.array((uv[0], uv[1], cam_focus)))
ray_world = Transform.normalize(view @ ray_local)
self.fb_rgba[y, x] = self.march(cam_pos, ray_world)
return self.fb_rgba
async def render(command: str, res: tuple[int, int]):
rasterizer = Rasterizer(Scene(command), res)
fb_output = rasterizer.rasterize()
rgba = np.clip(fb_output * 255.0, 0.0, 255.0).astype(np.uint8)
return Image.fromarray(rgba, mode="RGBA")

View File

@ -12,7 +12,7 @@ async def render(command: str, res: tuple[int, int]):
PATH = pathlib.Path(__file__).parent / "shaders" PATH = pathlib.Path(__file__).parent / "shaders"
with (PATH / "vert.glsl").open(encoding='utf-8') as f: with (PATH / "vert.glsl").open(encoding='utf-8') as f:
vs = f.read() vs = f.read()
ctx = moderngl.create_context(standalone=True) ctx = moderngl.create_context(standalone=True, backend='egl')
ctx.gc_mode = "auto" ctx.gc_mode = "auto"
try: try:
program = ctx.program( program = ctx.program(
@ -32,4 +32,5 @@ async def render(command: str, res: tuple[int, int]):
fbo.use() fbo.use()
fbo.clear(0.0, 0.0, 0.0, 0.0) fbo.clear(0.0, 0.0, 0.0, 0.0)
vao.render(moderngl.TRIANGLES) 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: def p_expr(self) -> str:
inv = np.linalg.inv(self.t) # + 1e-5 * np.identity(4, dtype=np.float32)) 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: class Object:
@ -83,9 +83,20 @@ class Object:
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
raise NotImplementedError 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 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):
@ -96,9 +107,14 @@ class Cube(Object):
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
self.size = ArgParser.as_vec3(args) 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]}))" 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):
@ -109,9 +125,13 @@ class Sphere(Object):
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
self.radius = ArgParser.as_float(args) 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})" 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):
@ -125,11 +145,20 @@ class Cylinder(Object):
self.radius = param[0] self.radius = param[0]
self.height = param[1] self.height = param[1]
def sdf_block(self) -> str: def sdf_block_glsl(self) -> str:
return ( return (
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):
@ -143,24 +172,34 @@ class Torus(Object):
self.r1 = param[0] self.r1 = param[0]
self.r2 = param[1] 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}))" 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):
def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None: def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None:
super().__init__() super().__init__()
self._h = _h self.h = _h
self._r = _r self.r = _r
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(args)
self._h = param[0] self.h = param[0]
self._r = param[1] self.r = param[1]
def sdf_block(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):
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") @make_obj("camera", "cam")

View File

@ -1,16 +1,8 @@
#version 330 #version 330
// compatibility
#define float4x4 mat4x4
#define float4 vec4
#define float3 vec3
#define float2 vec2
const float EPS = 0.001; const float EPS = 0.001;
const int MAX_ITER = 128; const int MAX_ITER = 128;
uniform vec2 u_resolution; uniform vec2 u_resolution;
out vec4 fragColor; out vec4 fragColor;
struct sdQuery { struct sdQuery {
float value; float value;
int obj_id; int obj_id;

View File

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

View File

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