atelier — 图像工作室,由短小的 Python 脚本驱动
安装 (Python ≥ 3.11): pip install git+https://github.com/Wexop/atelier (更新: pip install -U git+...)
检查: atelier palette · atelier brushes · atelier fx (签名).
循环(始终)
- 选择样式指南(见下方样式),并阅读它;没有合适的 →
styles/new-style.md。 - 编写
scene.py(30–80 行,from atelier import *,构建一个 Canvasc,以c.save(__file__[:-3] + ".png")结束)。 atelier render scene.py --preview→scene_prev.jpg(512 像素,真正的低分辨率渲染,跳过保存)。阅读图像。atelier inspect scene.py→scene_sheet.jpg:查看,5 值研究 + 三分法/焦点,剪贴图,直方图,调色板,4 个自动缩放 (锯齿、条带、噪点、高光)标记为--crop x,y,w,h,度量 + 红色标志。阅读它。 缩放:atelier render scene.py --crop x,y,w,h。- ≥3 次查看并修复的回合(
atelier compare old.png new.png用于 A/B 比较),然后atelier render scene.py(最终版;--scale 2,--seed N→SEED)。 错误打印一行scene.py:LINE: Type: message(--debug用于回溯)。
约定
- 绘制坐标 = 画布像素(y 向下)。字段使用归一化的 x,y ∈ [0,1]。颜色:"#hex",CSS 名称,(r,g,b) 0–1 或 0–255。
- 绘制源(fill=, color=, stroke=)= 颜色 | Field | f(x,y) | 数组 H×W×3/4。遮罩 = Field | Shape | 数组 | f(x,y)。
- 所有随机都需要
seed=。预览以低分辨率渲染:图像相同,仅在最终版本中更锐利。
画布与图层(核心)
c = Canvas(w, h, bg=None, pixel=False) # pixel=True:无抗锯齿,固定比例,最近邻导出
L = c.layer(blend="normal", opacity=1, mask=None) # 新图层,成为当前图层;c.use(L) 切换回来
blends: normal multiply screen overlay soft_light hard_light add subtract color_dodge color_burn darken lighten difference hue saturation color luminosity
c.fill(src, mask=None) · c.erase(mask) · c.place(img|path, x=None, y=None, scale=None(cover), rot=0, fit="cover"|"contain", blend, opacity) -> layer
c.apply(*fx, mask=None) # 合并所有图层,然后效果(最终调色);L.apply(*fx, mask=None) = 单个图层
c.save(path, quality=92, scale=1, smooth=None) · c.preview(path, width=512) · c.crop_preview(path, x, y, w, h)
c.flatten() -> H×W×4 float · L.rgba (get/set) · c.w c.h (逻辑) c.W c.H c.s (光栅比例)
字段(Field)—— 惰性,可组合:+ - * / ** > <, np.sin(f), f.ramp() f.warp() f.scale() f.rotate() f.move() f.remap(lo,hi) f.clip() f.within(shape|(x0,y0,x1,y1))
X Y radial(cx,cy) angle(cx,cy) · Field(lambda x, y: ...)
perlin/simplex/value(scale=4, seed) · fbm(scale=4, octaves=5, lacunarity=2, gain=.5, seed, base="perlin")
ridged(scale=3, ...) · turbulence(scale=4, ...) · worley(scale=6, mode="F1"|"F2"|"F2-F1"|"id", seed)
domain_warp(f, strength=.1, scale=2, seed) · aastep(f, edge=.5) · smoothstep(e0, e1, v) · mix(a, b, t)
patterns [0,1]: wood(rings=14, angle) marble() clouds(scale, cover=.5, sharp) terrain_height() water_ripples() fabric(scale=60) paper() stars(density=60, size)
gradient(stops, angle=90 (从上到下;0 = 从左到右), kind="linear"|"radial"|"conic", center, radius)
ramp(f, stops) / f.ramp("sunset") · stops = palette 名称 | ["#..", ...] | [(pos, col), ...] (OKLab 插值)
palette("name") 名称: sunset night ocean forest desert earth fire ice pastel neon vapor viridis wood marble mono retro16 green4
cosine_palette(a, b, c, d, n=8) · rgb2hsv hsv2rgb rgb2oklab oklab2rgb
shade(height, light=(azimuth, elevation), strength=1, color=None, ambient=.25, spec=0) # 无颜色 → 灰色,flat=0.5(在覆盖图层上使用)
masks: top(f) bottom(f) left(f) right(f) band(y0, y1) zone(x0, y0, x1, y1) disc(cx, cy, r) (所有 soft=)
curves: f.ridge_line(y=.6, amp=.15) / curve(lambda x: y) → c.fill_below(curve, src) / fill_above · curve.below()
向量与文字(vector)—— Shape = skia 路径,同时也是 Field 遮罩;布尔运算 | & - ^
rect(x, y, w, h, r=0) ellipse(cx, cy, rx, ry) circle(cx, cy, r) polygon(pts) line(x1,y1,x2,y2 | pts) regular(cx,cy,r,n)
star(cx, cy, r, r2, n=5) arc(cx, cy, r, start, sweep) spline(pts, closed) svg("M .. C .. A .. Z") union(*shapes)
shape.move/rotate(deg)/scale/outline(width, dash)/soft(feather)/points(n)/bounds/length
c.draw(shape, fill="black", stroke=None, width=2, cap="round", join="round", dash=(on, off), feather=0, opacity=1)
c.rect/ellipse/circle/polygon/line/spline/path(d)(..., **same style)
c.text(txt, x, y, size=32, fill, font="sans"|"mono"|"serif"|family|path.ttf, weight="regular"|"semibold"|"bold"|int,
align="left"|"center"|"right", valign="baseline"|"top"|"middle"|"bottom", spacing=em, line_height=1.2, italic, fit=width_px, kern=True)
text_shape(...) → Shape · text_size(txt, size) → (width, cap_height) · c.text_on_path(txt, shape|points, size, align, lift, fit, fill)
c.text_box(txt, x, y, w, h=None, size=18, align="left"|"justify"|"center", line_height=1.4, fit=True) # **b** *i* [#c33]colour[/]
c.title(txt, x, y, size=96, style="shadow"|"extrude"|"outline"|"inline"|"neon", fill, color2)
画笔(brush)
c.stroke(points|Shape, brush="round", size=12, color, pressure=[...]|f(t)|float, opacity, jitter, spacing, taper=(a, b), seed, **params)
brushes: round round_soft airbrush ink(speed) chalk(tooth) bristle(n, var, gaps) oil(impasto, pickup) watercolor(edge, granulation, bleed)
c.paint_like(ref, brush="oil", sizes=(48,24,12), tol=.08, angle=0, seed) # 用笔画重新绘制参考(Canvas/Field/image)
c.scatter(what, n, region=mask, size=(a, b), color=col|list|palette|Field, color_jitter, rot=(a, b), seed)
what: dot soft star grass leaf petal square person | Shape (单位大小在 0,0)
c.hatch(region, angle=45, spacing=8, width=1.2, color, tone=Field (0=暗 → 粗), cross=False, wobble)
km_mix(a, b, t) 类似颜料的混合(蓝色+黄色 = 绿色) · L.relief = 1.0(油画肌理光照强度)
效果(fx)—— c.apply(...) / L.apply(...);大小以画布像素为单位
blur(r) motion_blur(len, angle) sharpen(amt) bloom(threshold=.75, radius=12, strength=.8, tint) glow(r, strength, color)
drop_shadow(dx, dy, r, color, opacity) inner_shadow(...) vignette(amount=.35, softness=.6) grain(amount=.04, size=1, seed)
chromatic_aberration(px) lens_distortion(k) exposure(stops) contrast(amt) levels(black, white, gamma) curves([(in, out), ...], channel)
hue_sat(hue_deg, sat_mult, light) saturation(x) color_balance(shadows=(r,g,b), mids, highlights) gradient_map(stops) invert()
tone_map("agx"|"filmic"|"reinhard", exposure, look="punchy") displace(field, strength) posterize(levels)
pixelate(size, palette=name|list, colors=N, dither=None|"bayer"|"fs", strength=.6) halftone(size, angle, ink, paper, cmyk=False)
god_rays(x, y, rays=14, length=.9, strength=.45, color, threshold=.7, occlude=True) lens_flare(x, y, strength=.6, color, ghosts=6, streak=.5)
depth_of_field(depth, focus, aperture=8) # depth 来自 r = c.render(scene)
match_palette("ref.jpg", amount) transfer_tone(ref, amount) # 从参考图像进行颜色/色调转移
风格化(stylize)—— 3D → 插画 + 绘画滤镜(用于 c.apply / L.apply 的效果)
r = c.render(s, alpha=True); c.cur.apply(toon(r, bands=3, shadow=.45, colors={id: col}), shadow_dots(r, size=5, lo=.3), outlines(r, width=2.5, inner=1))
engraving: L.fill(PAPER, mask=r.alpha); L.apply(render_hatch(r, s, spacing=4.5, layers=3), outlines(r, width=2)) # 阴影线跟随 3D 表面
kuwahara(radius=6) oil · xdog(sigma=1, eps=.9, ink, paper) ink lines · pencil() · linocut(lines=7, angle) · edge_smooth(4)
watercolorize(bleed=2, edge=1, granulation=.4, ink=0..1) · stained_glass(cell=26, lead=3) · mosaic(tile=10) · shading(r) → (H,W) light
3D(sdf)—— y 向上,单位 ≈ 米,基本体以原点为中心
sphere(r) box(w, h, d) round_box(w, h, d, r) cylinder(r, h) capsule(a, b, r) torus(R, r) cone(r1, h, r2) plane(y, normal)
ellipsoid(rx, ry, rz) lathe([(radius, y), ...] 从底部到顶部, thickness=None|shell) extrude([(x, y), ...], depth)
ops: a | b, a & b, a - b, a.union(b, k=smooth) .inter .sub · .move(x,y,z) .rotate(x,y,z deg) .scale(k|(kx,ky,kz))
.onion(t) .round(r) .twist(k) .bend(k) .repeat(period, count) .mirror("x") .elongate() .displace(noise3(scale, octaves, seed), amount)
Mat(color | 2D Field (proj="tri"|"xz"|"xy"|"zy", tex_scale) | f(p, n)->(N,3), rough=.5|Field|f(p,n), metal=0, glass=0, ior=1.45,
sss=0, sss_color, emit=None, power=1, bump=.3, bump_scale=60, local=True (纹理跟随物体), sheen=.6,
wear=[smudges(.5), scratches(.5, density=40), dust(.3), edge_wear(.5, width=.01, color)])
s = Scene(); id = s.add(sdf, Mat(...)); s.camera(pos, target, fov=40)
s.sun(dir_toward_sun, color, power=3, soft=.04) · s.point(pos, color, power, radius) · s.area(pos, size=(w,h), normal, color, power, pattern="window"|"softbox", visible)
s.studio(key, target, size, power=45, window=True, fill=.2, bg) · s.sky(zenith, horizon, ground, power) · s.fog(color, density)
r = c.render(s, aa=1|2, exposure=0, look=None|"punchy", alpha=False) # 快速光线步进(直接光 + AO + 1 次反射)
r = c.pathtrace(s, spp=16, bounces=4, denoise=True, aperture=0, focus=None|dist|(x,y), seed=SEED) # 逼真:全局光照,多次折射玻璃,自适应抗锯齿
r.depth r.normal r.id r.mask(id) r.focus_at(x, y, c.s) r.linear r.spp r.time · denoise(img, albedo, normal, depth)
自然(nature, terrain)
h = terrain(384, kind="alpine"|"hills"|"mesa"|"island"|"dunes", seed, erosion=1, flow=False) # 侵蚀高度图 [0,1];erode() thermal()
c.terrain_map(h, style="relief"|"topo"|"shaded", water=.2)
v = c.landscape(h, cam=(x, alt, z), yaw, fov, horizon=.58, sun_elev=10, sun_az=30, haze=1, clouds=.35, water=.1, waves=.02, snow=.7, seed)
v.depth v.water v.project(x, y, z) -> (px, py, px_per_m) v.ground(x, z) # 在 3D 地形上放置 2D 树木
c.sky(sun_elev=8, sun_az=0, horizon=.6, clouds=.3, haze=1) · c.sea(sun_elev=6, waves=.12, wave_len=60, foam=.5, clouds=.25)
c.tree(x, y, height_px, kind="oak"|"pine"|"birch"|"palm"|"bush"|"dead", season="summer"|"spring"|"autumn"|"winter", haze=0..1, seed)
c.grass(x, y, w, h, n) c.flower(x, y, size, kind="daisy"|"poppy"|"tulip"|"bell") c.fern(x, y, size) · tree3d("oak", 4, seed) → (trunk, leaves) SDFs
scene_sky(s, sun_elev, sun_az) # 3D 场景上的物理天空
c.anime_cloud(x, y, w, h=.55w, palette="day"|"sunset"|"dusk"|"storm"|(shadow,mid,light,rim), light=(az, elev), bands=3, rim=.7, wisps, edge, bottom, seed) # 赛璐珞风格积云,平底在 (x, y)
c.anime_tree(x, y, height, kind="round"|"tall"|"umbrella", palette="summer"|"spring"|"autumn"|"sakura"|"dusk"|(sh,mid,lt,rim,trunk), light, width, leafy=.6, seed) # 赛璐珞风格树
c.anime_mountains(y_foot, height, peaks=4, x0, x1, palette="day"|"dusk"|"autumn"|(sh,mid,lt,snow,snow_sh,haze), light, snow=.35, haze=.6, sharp=1, depth=.35, bands=3, seed) # 侵蚀地形,正视图,赛璐珞色调
c.sparkles(n, region=mask, size=(6, 16), color, rays=4, glow=.6, seed) # 星光闪烁(水面,雪地)
人物(figure)—— 脚在 (x, y),高度 = 站立像素(动物:肩高)
c.figure(x, y, h, pose="stand", style="flat"|"line"|"silhouette"|"stick"|"ink"|"chalk", kind="human"|"cat"|"dog"|"horse"|"bird", seed)
poses: stand idle walk run sit wave point cheer jump dance think hips phone reach · phase=0-1(行走周期) view="front"|"3q"|"side"|"back"
body=adult|teen|child|elder build=slim|heavy outfit="tee pants|coat skirt|dress" hair=short|bob|long|bun|curly skin=SKIN name colors={...}
figure_shape(...) → Shape · figure_joints(...) → {"hand_r": (x, y), ...}(附加道具) · figure_sdf(pose, height=1.75, style="mannequin")
c.crowd(n, box=(x0, y0, x1, y1), horizon=y, height=near_px, tint=(haze, .5), dogs=.1, seed) # 透视缩放的人群
检查(inspect)—— 判断数字,而不只是缩小后的视图
m = metrics(c|path|arr) # noise (<1 干净,>4 噪点) aliasing (% 硬边,>35 锯齿) banding clip_shadows/highlights flags[...]
contact_sheet(c, "sheet.jpg") · compare(a, b, "ab.jpg") · guides(c) / composition(c) → 焦点,平衡,倾斜 · analyze_reference("ref.jpg")
样式 —— 在编写场景前阅读匹配的指南
指南位于此文件旁边的 styles/ 目录中(如果缺失:https://raw.githubusercontent.com/Wexop/atelier/main/styles/<name>.md)。
每个指南提供配方、规则、陷阱和示例脚本。多个指南可以混合使用(例如 anime + flat)。
anime.md— 手绘动漫景观:赛璐珞云、树木、山脉、边缘光、神光、闪光oil.md— 油画(paint_like)和水彩渲染photoreal.md— 光线追踪的静物/产品、地形景观、海洋和天空comic.md— 清晰线条 / 从 3D 生成的卡通画、雕刻、墨水滤镜flat.md— 扁平矢量图、海报、排版、人物、剪影场景pixel.md— 带调色板和抖动的像素艺术abstract.md— 从扭曲场生成的生成式抽象new-style.md— 没有合适的指南:研究外观,映射到基本元素,绘制,然后保存新的styles/<name>.md
陷阱
c.layer()会更改当前目标:后续的c.text/...会进入该图层(如果它是multiply则效果不佳)。调用c.layer()或c.use(L)。c.apply()合并所有图层。使用L.apply()来保留图层。shade(strength)依赖于场的频率:大的形体 ≈1,细小的纹理(织物)≈.05。tone_map在 2D 艺术上会使纯白色变成灰色——它用于 HDR/3D;在插画中使用curves/levels。- 硬阈值会产生锯齿:使用
aastep/smoothstep,而不是f > .5,对于可见的边缘。 - 噪点
scale= 每画布高度的特征数。形状坐标 = 画布像素;场坐标 = 0–1。 - 成本:
pathtrace≈ 30–60 µs/路径(1200×800 @ 8 spp ≈ 2 核上 5 分钟,预览的 spp 少 4 倍)— 用c.render打草稿; 低 spp 时高光金属/玻璃上会有闪光(提高 spp)。aa=2≈ 4×。许多 SDF 部分(人物、tree3d、displace)追踪较慢。 - 玻璃:
rough=0用于清澈(默认 rough .5 = 磨砂)。在c.render中玻璃只弯曲一次;使用pathtrace进行真实的折射。
…
[已截断以供翻译]