manual 补充,尝试迁移 cpu backend
This commit is contained in:
@ -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_ty、op_ty 分别为物体类型(如 cube、sphere、torus 等)与变换类型(如 pos、rot)。
|
其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。
|
||||||
|
|
||||||
# 特殊说明
|
# 特殊说明
|
||||||
<op_ty> 不包含 scale,因为本工具基于 SDF 渲染,非正交的变换会破坏 SDF 的性质。
|
`<op_ty>` 不包含 scale。非正交的变换会破坏 SDF 的性质。
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
109
konabot/plugins/marchtoy/cpu_render.py
Normal file
109
konabot/plugins/marchtoy/cpu_render.py
Normal 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")
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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]])
|
||||||
|
|||||||
@ -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
|
|
||||||
Reference in New Issue
Block a user