跳转到内容
查看源码

分步教程:grayscale

examples/grayscale 是架构计划里的“第一个证明”:

gradient -> grayscale -> pixelate -> output.ppm

本教程在仓库根目录下进行,所有输出都是实际运行得到的。

kernels/gradient.ent——一个生成器,没有输入,所以场景必须给它定义域:

/// A generator: no inputs, so the scene must give the node a domain.
kernel image.gradient@1 {
requires { field.write; }
param depth: Float = 0.5;
output result: Image;
compute {
let uv = coord() / (domain() - 1.0);
return vec4(uv, depth, 1.0);
}
}

kernels/grayscale.ent——按亮度权重转成灰度,amount 控制混合程度,fn luma 是 Kernel 内部的函数:

/// Edit the weights or `amount` while `entelechy run --watch` is running.
kernel image.grayscale@1 {
requires { field.read; field.write; }
input source: Image;
param amount: Float = 1.0;
output result: Image;
fn luma(c: Vec4) -> Float {
return dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));
}
compute {
let c = sample(source);
let g = luma(c);
return mix(c, vec4(g, g, g, c.a), amount);
}
}

kernels/pixelate.ent——用 fetch 读取任意位置(块的原点),而不只是当前元素:

kernel image.pixelate@1 {
requires { field.read; field.write; }
input source: Image;
param size: Int = 8;
output result: Image;
compute {
let block = float(max(size, 1));
let origin = floor(coord() / block) * block;
return fetch(source, origin);
}
}
// gradient -> grayscale -> pixelate
use "kernels/";
node source: image.gradient@1 {
domain = vec2(128, 128);
}
node gray: image.grayscale@1 {
source <- source.result;
amount = 1.0;
}
node blocks: image.pixelate@1 {
source <- gray.result;
size = 8;
}
output result <- blocks.result;
终端窗口
entelechy run examples/grayscale/scene.esn --out output.png
scene loaded
published image.gradient@1 g1 from examples/grayscale/kernels/gradient.ent
published image.grayscale@1 g2 from examples/grayscale/kernels/grayscale.ent
published image.pixelate@1 g3 from examples/grayscale/kernels/pixelate.ent
scene loaded
evaluated in 1.241085ms: executed [gray+source, blocks], reused 0 -> output.png

每个 Kernel 发布时得到一个代数(g1、g2…),之后的每次替换都会递增。

grayscale 的输出(放大 3 倍)

终端窗口
entelechy replay examples/grayscale/tests/grayscale.replay
examples/grayscale/tests/grayscale.replay: ok (1 frames, 5 checks)

测试文件检查了几个像素的颜色:同一块内的像素同色,越往右下角越亮。

scene ../scene.esn
# The gradient in gray, in blocks of eight: a block's pixels are one
# gray, the next block's another, lighter toward the far corner.
frame 0.0
expect 0 0 = 0.036 0.036 0.036 0.002
expect 7 7 = 0.036 0.036 0.036 0.002
expect 8 0 = 0.049 0.049 0.049 0.002
expect 64 64 = 0.504 0.504 0.504 0.002
expect 127 127 = 0.913 0.913 0.913 0.002
终端窗口
entelechy graph examples/grayscale/scene.esn
// plan: 2 group(s)
// gray + source: fused.gray@1 vec2(128, 128)
// blocks: image.pixelate@1
// gray runs apart: `blocks` reads it neither at its own element (`sample`) nor at one offset of a row

gray 只在当前元素处 sample 了 source,于是两者被融合成一个 Kernel;blocks 用 fetch 读取别处的像素,所以 gray 必须先单独算完。graph 会说明每个“分开运行”的原因。

终端窗口
entelechy trace examples/grayscale/scene.esn --node gray --at 64 64
gray (image.grayscale@1), element (64, 64), as the evaluation at time 0 runs it
c: Vec4 = vec4(0.503937, 0.503937, 0.5, 1)
g: Float = 0.50365275
call luma #1
-> result: Float = 0.50365275
-> result: Vec4 = vec4(0.50365275, 0.50365275, 0.50365275, 1)
终端窗口
entelechy run examples/grayscale/scene.esn --out output.png --watch

在它运行时依次尝试:

  1. 把 scene.esn 中的 amount = 1.0; 改成 0.5 并保存——画面重新计算,带一半颜色。
  2. 在 grayscale.ent 里写一个错误(例如把权重改成 nope)——会看到 rejected …:8:59: \nope` is not defined`,输出保持不变。
  3. 把 pixelate.ent 的 size 默认值改为 16——注意场景里写了 size = 8;,节点的值优先于 Kernel 默认值。
终端窗口
entelechy run examples/grayscale/scene.esn --save gray.result=gray.png --out output.png
saved gray.result -> gray.png

--save <节点.端口>=<文件> 可以导出任何节点的输出,格式由扩展名决定(.png、.ppm、.field、.wav、.csv、.json…)。这也是“制作资源”的方式。

gray 节点的输出

终端窗口
entelechy run examples/grayscale/scene.esn --backend cpu --out output.png --profile
gray 0.952 ms cpu
blocks 0.798 ms cpu

--backend 可选 cpu(参考解释器)、jit(默认)、gpu。没有 GPU 适配器时,--backend gpu 会直接报错 gpu unavailable: No suitable graphics adapter found…;Player 默认的 best 会在没有设备时回退到 JIT。见后端。

终端窗口
entelechy serve examples/grayscale/scene.esn

然后在 stdin 上逐行输入(→ 是请求,← 是应答;这里在副本 gp/ 上操作):

→ {"id":2,"op":"publish","source":"kernel image.pixelate@1 { requires { field.read; field.write; } input source: Image; param size: Int = 16; output result: Image; compute { let b = float(max(size, 1)); return fetch(source, floor(coord() / b) * b); } }","expect":3}
← {"id":2,"ok":{"generation":4,"id":"image.pixelate@1"}}
→ {"id":3,"op":"compose","edits":[{"node":"blocks","member":"size","text":"size = 4;"}]}
← {"id":3,"ok":{"events":[{"kind":"scene_loaded"}],"path":"gp/scene.esn","version":"bdced0a164ed8778","written":true}}

publish 带 expect: 3 表示“只有当前代数仍是 3 时才替换”;compose 用 edits 修改了场景文件中 blocks 节点的 size,并真的写回了磁盘。完整说明见 Agent 协议。

  • Kernel 按 id 绑定,场景只负责连线;
  • 融合、计划、追踪都可以直接观察;
  • 错误的改动被拒绝而世界照常运行;
  • 回放就是测试;
  • 同一个程序可以在三个后端上运行,也可以被 Agent 通过协议修改。

接下来可以浏览示例一览,或者开始读手册。