mcpField
R
roblox-vfx

roblox-vfx

github.com/syphodev-yt创意与媒体13153 次安装4 天前更新
代码仓库

关于 roblox-vfx

roblox-vfx 是用于设计 Roblox 视觉效果的技能,可在浏览器模拟器中逐帧审查并导出 Studio 套件。它还提供独立流程,可从自有资源包提取发射器配置并生成 VFXKit 模块。

主要功能

  1. 01在模拟器中以定义形式设计效果
  2. 02为效果生成纹理和网格
  3. 03在模拟器中逐帧审查效果
  4. 04将效果导出为 Studio 套件
  5. 05从自有资源包提取发射器并生成 VFXKit

适用场景

  1. 01制作和调整动漫风格的 Roblox 效果
  2. 02检查效果在各帧的时序与外观
  3. 03为 Roblox Studio 准备效果
  4. 04复用自有免费资源包中的发射器配方

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/syphodev-yt/roblox-ai-skills --skill roblox-vfx

安装后在 Claude Code 中输入/roblox-vfx确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LOCAL.template.md
references/
BEATS.md
CharacterVFX.server.lua
DESIGN.md
HitSystem.luau
property-surface.md
VFXKit.lua
references/corpus/
CATEGORIES.md
SCHEMA.md
STATS.json
STATS.md
references/corpus/tools/
contact.py
events.py
measure.py
palette.py
strip.py
vtt2txt.py
references/styles/
anime_cel.md
realistic.md
STYLES.md
stylised_game.md
scripts/
assemble_kit.py
audition.py
classify_lib.py
compose.py
contact_grid.py
critic_gate.py
curate.py
emit_luau.py
emit_scenes.py

详细介绍

Roblox VFX,无头模式

第一次在这台机器上运行? 请阅读此文件夹中的 LOCAL.md。如果该文件不存在,请运行 roblox-skills-setup 技能(或将 LOCAL.template.md 复制为 LOCAL.md 并填写)。 LOCAL.md 保存你的路径和工具位置;它属于你,不会被分享。

此技能有两种路线:

  1. 制作(常规路线,也是看起来最专业的路线)。设计特效,在浏览器模拟器中将其构建为定义,逐帧评估,然后导出 Studio 套件。所有纹理和网格均由此技能生成,因此你发布的所有内容都属于你。从 “标准” 到 “每次都制作高品质 VFX” 章节。
  2. 提取(用于快速原型和占位内容)。从你拥有的免费套件中读取每个发射器,将配方保存在自己的库中,然后生成一个 VFXKit 模块。从 “提取路线” 章节开始往下阅读。

首次运行:构建纹理和网格(每台机器只需一次)

纹理库和网格是生成的,因此不包含在下载内容中。请构建一次 (约 250 MB 的 PNG 文件,需要几分钟):

cd sim/tex && python make_textures.py && python make_magic_tex.py && python make_painted.py
cd lib && python build_library.py        # 2,000+ library textures, run it from this FILE, not stdin
cd ../../mesh
blender -b -P make_meshes.py             # use the full path to blender.exe from LOCAL.md if it is not on PATH
blender -b -P make_props.py
blender -b -P make_dragon.py
cd ../tex && python make_prop_tex.py     # skins for the prop meshes (needs meshes/props.json)

Python 需要 numpy、scipy 和 Pillow(pip install numpy scipy pillow)。然后运行 python sim/serve.py 并打开打印出的地址:三个 已完成示例(Crimson Detonation、Eclipse Rend、Riptide Lance)应能播放。


标准(100 个招式的动漫风格展示,已于 2026-10-29 在 Studio 中验证)

此技能旨在达到的标准:使用此技能制作 20 个角色和 100 个招式,并在 Studio 中录制和验证。评估每个新特效时,都应以此为标准,绝不低于此水平。

达到此标准的关键。以下各项都必须做到:

  1. 每个元素都要货真价实(见下文 RULE ZERO)。武器、生物和棺材都必须是网格,而猛击必须留下真正的弹坑和碎屑。
  2. 终极技要加入动漫式大场面效果: 撞击帧、FOV 冲击、聚焦线、遮幅和镜头切换(下文的模拟动作 fov、overlay、bloom)。
  3. 最后为每个特效进行润色: sim/effects/fxpolish.py、polish(d)。
    • 能量穹顶要变成饱和度最高颜色的 ForceField 外壳,包裹着一个尺寸为 0.6x、先行消散的 Neon 核心。
    • 弹坑边缘采用竞技场的石头颜色。
    • 光环火焰和火花要放在 Part 上,而不是 Attachment 上。
    • 彩色残影的透明度至少为 0.6。
    • 持续烟雾要更小、更透。
  4. 使用饱和色,绝不使用苍白的混合色。 苍白的穹顶会呈现透明玻璃效果,苍白的核心会泛白。表现黑暗招式时,应让世界变得苍白,以黑色表现黑暗,并且不要在黑色上添加辉光。
  5. 目标要像真实身体一样做出反应(见下一节)。如果被击中的身体仍然直立漂浮,无论粒子效果多出色,呈现出来都会很不自然。
  6. 镜头要框住完整动作。 检查每次切镜头的第一帧和最后一帧,结尾特写保持在 4-5 studs,镜头绝不能进入施法者体内。

录制前的门槛。每项检查都必须通过,依据数值判断,不凭肉眼:

检查通过标准
python sim/tools/sa_parent_check.pyMeshPart 之外的 SurfaceAppearance 数量为 0
lune run sim/tools/lune/build_check.luau <kits/*.luau>(从 aftman.toml 或 rokit.toml 列有 Lune 的文件夹运行)每个套件均通过
lune run sim/tools/lune/compile_check.luau <scripts>每个脚本均通过
node sim/tools/phys_audit.js [prefix]不出现 gap、sink、hover 或 STILL DOWN 行
使用 sim/snap.js 在每个招式的节拍处截取模拟画面,并为每个角色拼成一张图至少检查过一次
在 Studio 中完整预热运行每个招式一次,然后再进行正式录制如果不这样做,新建的 MeshPart 与 SurfaceAppearance 配对会显示“正在生成”
正式录制“正在生成”警告为 0;目标未被特效或施法者碰触;起身动作的最低点至少为 -0.05;最差帧约为 40 ms

一个像真实身体一样作出反应的目标(在 Studio 中测量)

经验证的模块是 references/HitSystem.luau。它会以被击飞、翻滚的 R6 身体产生击退效果, 以下每条规则都修复了在 Studio 中发现的问题:

  • 让身体瘫软需要三个步骤: PlatformStand = true,然后 ChangeState(Enum.HumanoidStateType.Physics), 接着 EvaluateStateMachine = false。仅禁用状态机会让 Humanoid 继续维持身体平衡。这就是“被击飞后又在空中站直”的问题。
  • 抬升用的 VectorForce 必须包含 HumanoidRootPart。 它占 R6 总质量 12.6 的 2.8,因此不包含它时,每次击飞的距离都会不足。
  • 使用碰撞组。 每个效果部件都放入 FX 组:工具包文件夹、其 _Actors 模型(通过 DescendantAdded 和 workspace.ChildAdded 连接),以及施法者。目标放入目标组,两个组之间不发生碰撞。工具包的碎片会削弱发射速度,施法者的四肢也会撞到身体。
  • 直立着地的身体需要被掀翻。 当速度低于 2 studs/s 并持续 0.25 s 时,或身体直立(UpVector.Y > 0.35)且速度低于 5 studs/s 并持续 0.4 s 时,视为着地。直立的身体会受到推力,并以 6 rad/s 施加 AngularVelocity,持续 0.4 s;1.2 s 后则会触发备用起身。
  • 起身时身体要稳稳落在地面上。 对地面进行射线检测(先检测竞技场,再检测地形)。根据 RootJoint 和每个 Motor6D(C0 * C1:Inverse(),半径为 Part1.Size)计算身体各部件的盒体。每帧进行 0.5 s 的 Quad Out 起身动画时,将根部放在最低角恰好贴地的位置。立即放置第一帧,避免出现一帧的弹跳。
  • 踉跄滑行时,根部高度绝不能低于地面 + 3。
  • 布娃娃效果必须是最后一次受击效果: 后续的受击反应会重新锚定身体。
  • 两招之间: 调用方会调用 HitSystem.recover(target),并等待 isStanding(最多 2 s),让画面中的身体先起身,然后等待 max(0.3, 0.8 - elapsed) s 再施放下一招。
  • 模拟器使用相同的身体模型: w.foe,模式包括 stand、fly、down 和 getup。 phys_audit.js 会对此进行检查。

在 Roblox Studio 中测量(2026-10-27):模拟器按以下数据校准

模拟器曾与 Studio 中真实帧画面对比,测试了三种复杂效果(水龙法术、凤凰俯冲、Crimson Detonation),并不断修正,直到能够预测 Studio 的表现。请以此为准,覆盖任何更早的说明:

  • ParticleEmitter 的 Size = 绘制宽度的一半(Size 为 4 时绘制宽度为 8 studs)。模拟器 按 2 x Size 绘制;构建器填写期望看到的宽度,并由 effects/roblox_units.py 在写入时将 Size 减半(sizeConvention: "roblox")。直接写入 JSON 的构建器必须调用 to_roblox_units()。
  • SurfaceAppearance 的发光效果会饱和,但不会产生泛光:白热效果应使用 Neon 或 LightEmission 粒子,而不是网格发光。发射颜色 = 表面颜色 x EmissiveTint。未受光照的表面在夜间会暗很多,因此夜间烟雾应增加发光效果。
  • PointLights 会在 Roblox 中照亮表面,但在模拟器中不会:应将它们淡出,否则附近的网格上会出现条带。
  • 不要在巨大的平面上使用实心核心纹理(否则会变成实心圆盘):使用 glow_falloff。
  • 加入时预加载所有模板,不要等到即将开始时才加载。
  • 网格上传流程:OBJ -> FBX 会使网格绕 Y 轴旋转 180 度。 Open Cloud 会拒绝 .obj,而常见的 Blender 转换方式(以 -Z 为前向 / Y 为上向导入,再以 -Z / Y 导出)会让 OBJ 的 -Z 方向对应 Roblox 的 +Z,因此每个网格都会倒着导入(凤凰尾部朝前飞;龙则以角朝前)。使用 sim/mesh/obj_to_fbx.py,它会在导出前额外旋转半圈。对称网格看不出这个问题,因此要用不对称网格检查方向(红色方块在 -Z,蓝色方块在 +Z)。
  • **要在 Studio 中验证修改,**将相同的 JSON 导出为工具包(sim/studio_kit.js),在相同时间点截取 Studio 画面,并将各帧并排比较:旧模拟器 | 模拟器 | Studio。

构建 100 个动漫风格招式的经验(2026-10-27 至 10-29)

静默失败:每个功能单独看都正常,放到别处却会出问题。

  • CFrame 补间会走较短的旋转路径(模拟中的球面线性插值 = TweenService),所以 260 度旋转会变成 -100 度,720 度旋转则完全不转。使用 {"do":"spin", target, axis:"X|Y|Z", rate:deg/s, time} 进行旋转;它会叠加在同一部件的补间或路径上。现在 fxmore.circle() 也使用它。
  • 按 ID 预加载 SurfaceAppearance 贴图。 对实例调用 PreloadAsync 不会获取 ColorMap:陨坑平面会显示为纯色方块,直到贴图流式加载完成。现在 kit 的 M.preload 会请求每张贴图以及 MeshId 字符串。
  • 新上传的资源可能会显示“Asset content representation is being generated”超过 30 分钟,而且 Studio 会在本次会话中一直保留这个失败状态。 生成似乎会在首次请求时开始。保存、重启窗口,然后重试。
  • 在大型球体上叠加白色增色效果会变成过曝圆盘(5-stud 的旋转球体、16-stud 的灵体球体)。缩小核心辉光,为旋涡着色(不要从白色开始),并在旋转的纹理外壳下使用非 Neon 主体。
  • 细长的纹理新月形带会消失; 波浪效果需要真正的新月网格,且材质应为 Neon(sim/mesh/make_meshes.py 中的 crescent_wall)。
  • 帧条必须预加载网格: 网格只会在部件首次显示时异步获取,因此同步帧条不会绘制新网格。现在 index.html 会在加载时获取每个 MeshId。app.ready() 会返回 Promise:请等待它完成。
  • 让摄像机取景覆盖整个动作: 模拟场景的一半问题都出在取景上(施法者被边缘裁切、刀刃伸到镜头前、摄像机位于区域球体内部)。检查每次摄像机切换的首帧和末帧。
  • Luau 每个函数最多允许 200 个局部变量: 包含约 200 个实例的 kit(由部件搭建的神龛、巨型灵体护甲)会编译失败。现在 studio_kit.js 会把模板实例保存在表中(I[n])。
  • Attachment 下的发射器会忽略其 Shape: Box 光环或向内的球形微粒都会在同一点生成(光环火焰出现在施法者躯干内;“风暴”般的剪影精灵变成了星爆)。有形状的发射器应直接放在 Part 下。
  • 冲击帧(带剪影的动漫风格黑白闪光):使用 Neon 半球 + 地面,并在施法者和目标身上添加 Highlight。从内部看,网格上的 Highlight 不会填充网格;而且 Highlight 会比部件提前一帧切换。极近特写中的双色剪影冲击帧会画成一个巨大的色块:特写时请使用纯色帧。实测:4 个终极技能中有 4 个使用了冲击帧,9 个普通招式中有 0 个使用。
  • 用于重头戏效果层的模拟/kit 动作:{"do":"fov", by, i, o}(FOV 冲击)、{"do":"overlay", kind:"lines"|"bars"}(ScreenGui 聚焦线 / 黑边)、{"do":"bloom"};Highlight 的 Adornee 为 "Foe" = 目标(h.target);震动和 FOV 通过同一个摄像机驱动器处理,该驱动器会移除上一帧的偏移量(早期的震动实现会产生漂移)。
  • 固定的绳索(触须、鞭子)不能用“路径 + 延迟”(这样会像蛇一样滑动):固定根部,并将绳索上的各点作为独立路径动画。
  • 在模拟中,叠加两个灰度 CC 会使色度反转(饱和度相加后低于 -1);现在已进行钳制。
  • Roblox 中,运行中的摄像机补间期间发生的摄像机切换会丢失(补间会持续写入 CFrame,直到结束;模拟则会取消它):现在 kit 会在每个摄像机事件发生时取消正在运行的摄像机补间。
  • 帧检查:sim/snap.js(eval(await (await fetch('snap.js')).text()); await snap(file, name, times))。
  • {"do":"timescale"} 会设置每个发射器的 TimeScale;0 = 时间停止,现有粒子会悬停,而冻结期间通过 Emit() 生成的粒子也会以冻结状态出现,因此被冻结的 FLARE 会像白色团块一样悬停:停止时间期间只发射火花/爆发效果,或者对 flare 使用 clear。此外还有 {"do":"target","hide"};actor 的 model = 另一个 rig 或 "@target"(真实目标的副本,关节会受驱动,并保留真实服装);关键帧 move(带平移的 Motor6D.Transform 会让肢体脱离关节)、scale(每帧调用 Model:ScaleTo,之后重新读取 C0/C1/Size)、fade(LocalTransparencyModifier)。
  • Roblox 中的 Enum.EasingStyle["Constant"] 会报错:kit 的 easeV 会将 Constant 视为阶跃关键帧,并在遇到未知样式时使用回退值。
  • 特写必须为 rig 的配件留出空间:帽子或 6.8-stud 的斗篷会占满 3-stud 的特写画面;结束镜头保持在 4-5 studs,并避开太阳方向。每个开场镜头都用 sim-snap 检查:FORM/CHANT/POINT 时若特写距离为 2-3 studs,镜头会进入施法者内部。
  • 重击目标时留下的烟雾会飘到结束特写镜头中:非终极技能请将 linger 设为关闭。
  • 黑暗光环不能发光: 在黑色火焰上叠加增色辉光层,会像红色棉花糖。将黑暗光环的 LightEmission 设为 0,让其包络层成为非增色的黑雾。
  • 能量穹顶内的 Neon 核心必须使用穹顶中饱和度最高的颜色。浅色混合会在每个穹顶上泛白;对于黑色穹顶或自发光主体,"noCore": True 会移除核心。
  • 模拟器会将 ForceField 材质渲染成白色增亮眩光,无论其颜色如何。ForceField 穹顶只能在 Studio 中评估。在 Studio 中,只有颜色足够饱和时,ForceField 穹顶才会显出颜色:浅色调会呈现为带白色边缘的透明玻璃,暗深红色则会像真正的外壳。润色阶段会为外壳选用饱和度最高的颜色。
  • Ball 部件的内部会被剔除。若要制作可以置身其中的天空效果,请使用 Neon 网格球体:它会自发光,因此即使内侧面的法线朝外,仍然看得见。
  • 世界变暗会让黑暗系招式难以辨认。让世界保持浅色且无色,把黑暗留作黑色。
  • 劈开天空的镜头需要密集的云层(约 100 团,每团 9-15 studs)。30 团零散的云看起来不像被劈开。
  • 带有柔和辉光 SurfaceAppearance 的 Neon 网格会像一团模糊的色块。普通 Neon 网格则能显出轮廓。
  • path 步骤每帧都会根据移动方向重建部件的旋转,并且只保留其 offset:在起始 CFrame 上设置的倾斜会被丢弃。请将倾斜设置在 offset 中(手里剑齐射原本侧着飞,变成了细细的暗线,修正后才恢复正常)。
  • Studio 中,旋转部件上的光迹粒子会变成垂直的“雨滴”状棒线。请将光迹放在独立载体部件上的 Trail 中,让载体沿相同路径移动,但不旋转。
  • 用 Neon 制作的生物召唤效果,如果身上带有大型火焰发射器,看起来会像黄色云团。使用普通 Neon、小型发射器,以及该生物的真实网格(用狗网格代替狐狸会变成一团;建模出高耳朵的狐狸则能一眼认出)。
  • SurfaceAppearance 只能作为 MeshPart 的子级:如果用普通 Part,Studio 会拒绝整个 kit。制作带纹理的薄板时,请使用 MeshPart(立方体网格)。Lune 的 build_check 能捕获此问题(它会对 CreateMeshPartAsync 进行桩替换),sim/tools/sa_parent_check.py 也可以。
  • 首次显示部件时,SurfaceAppearance 贴图可能会有约 1 秒显示为无纹理的浅色,即使已经预热播放过也一样。请在 t=0 时将部件放在某个隐藏位置绘制(例如地板下),这样贴图就能在对应节拍到来前加载完成。 角色动作(施法者动画)在模拟器和 studio_kit.js 中:def.rig 键驱动真实施法者的 Motor6D;def.actors = 施法者的运行时克隆(影子克隆);键 stretch: {RightArm: k | [k, width]} 可拉长/加粗肢体,同时固定关节末端(在 Lune 中精确);{"do":"ghost", parts, keep} = 为选定部件生成残影;一个 Adornee 为 "Dummy" 的 Highlight 会包裹施法者;{"do":"face"} 会调用 h.setFace;{"do":"hit", damage, knock, reaction, stun, time, ultimate} 会交给游戏的命中系统(knock = 确定性位移,因此可以将效果放在目标即将到达的位置)。

在仅模拟器展示中学到的经验(玩具城镇上的灾难,视频由模拟器渲染)

任何“无需 Studio 展示效果”的需求,都值得了解这些经验。

  • **fxpolish.polish() 会将每个火焰外壳 lumpy_sphere 变成 ForceField 外壳 + Neon 核心,而模拟器会将较大的 ForceField 渲染成白色团块。**黑洞的冲击波外壳和陨石的火焰穹顶都被渲染成白色椭圆。对于模拟器渲染的视频,请写入两个文件:<name>.studio.json 包含完整的 polish(适用于 Studio),<name>.json 则省去 domes() 的所有步骤(smoke(ghosts(auras(craters(d)))))。
  • TL.flash() 会将 Brightness 写回 0,覆盖它进行闪烁的色调。如果场景通过色调渐变变暗(例如风暴),每次闪光都会取消变暗效果:让闪光使用自己的 grade() | {"name": "Flash"}。
  • 网格的 Size 会缩放其包围盒,因此在同一共享空间中建模的部件(身体、下颌、腿)只有在每个部件都通过 place(origin, centre * K) 放置并通过 box_of(mesh, K) 调整尺寸时,才能保持对齐。
  • **模拟器中的布娃娃身体:**每个身体包含三个网格(站立、惊慌、挥舞),通过 Transparency 切换。沿着 path 飞行时为挥舞姿势添加 spin(结果具有确定性,因此着陆位置已知)。着陆后将其平放,发射眩晕星星,然后在着陆点切回站立网格(起身)。
  • **在精简前,检查每个摄像机位置是否会被场景建筑遮挡。**最初的六个镜头中有四个,镜头要么位于房屋内部,要么画面被屋顶占满。宽幅特效(232-stud 的波浪)需要将侧面摄像机放在其宽度范围之外。
  • y=0.03 处的地面会在远处与模拟器自带地板产生 Z 轴冲突(草地会分块闪烁)。将地面放在 y=0.2 处,贴花放在 0.3 以上。
  • 视频:app.captureFrame(t, 1920, 1080, q) 向 /save 发送 POST 请求时,前台标签页每秒约能处理 3 帧,在后台标签页中速度会慢得多。在页面中启动不等待的循环(将进度保存在窗口变量中),从后台 shell 循环检查帧数,然后用 ffmpeg -framerate 60 -i <name>_%05d.jpg 为每种效果生成视频片段和标题卡,再用 -c copy 拼接。长时间渲染可能导致标签页崩溃(在第 5,343 帧/共 5,808 帧时发生过),因此应等待一个会标记卡顿的循环,并从最后一帧继续。对每一帧的 POST 请求都要重试:有一次单个 fetch 失败就导致首次渲染中断。从 Python 运行 ffmpeg 时使用 -nostdin、stdin=DEVNULL 和 capture_output=True:在后台 shell 中继承句柄会导致崩溃。
  • **小型生物网格无法缩放成巨型 Boss。**其 1024 纹理烘焙结果在放大 30 倍后变得斑驳模糊。应改为构建该设计的关节式方块版本,并用 CFrame 渐变通过正向运动学进行摆姿势,避免关节与 swing 互相冲突。

总结:在真实游戏中实现 20 种灾难效果(2026-11-02)

先制作模拟表格,再在真实游戏中逐一捕获每种效果。只有在游戏中才能发现的问题:

  • 根据游戏实际启动时的镜头来制作效果。 效果是在缩放值为 165 时评估的,而游戏启动时为 90:一条高 58 个 stud 的龙飞出了画面。要么让高大的物体保持在视锥内,要么为效果暂时拉远镜头,然后再恢复视野。
  • 超过 200,000 个字符的套件源代码无法安装。 分步关键帧和大量一次性载体很快就会达到这个长度:外观相同的部件应复用同一个载体,而不是每次都创建一个。
  • 模拟器默认颜色的体积烟雾从上方看像黑色团块。 灰度保持在约 0.45 以上,透明度设为 0.3,LightInfluence 设低,并大幅降低发射率。坑洞或光束上方的烟雾会遮住效果。
  • 带纹理的贴图上使用自发光会过曝成一片惨白。 熔岩贴图的强度应设为 0.5-0.8,并使用细窄的裂纹:噪声带覆盖的地图范围远超预期。
  • 评估真实游戏时,Studio 必须处于焦点状态,且不能最小化。 失去焦点时,游戏会变得极慢,静止画面还会重复;最小化时,屏幕捕获会失败。捕获时隐藏教程卡片,并将坠落物瞄准城镇中心,而不是人群。

规则零:绝不走捷径

每种效果都要做到尽可能好。效果需要什么,就制作什么。 法术如果是生物,就制作真正的生物网格;武器就制作真正的武器;书就制作真正的书;新外观就制作专属纹理。绝不要因为更快就用现有的通用部件(光束、团块、方块、复用纹理)代替。捷径正是让效果变得乏味的原因。这条规则优先于时间和令牌预算。

促使我们制定这条规则的失败案例:一个水龙法术最终被做成了层叠的水光束,头部则是一个凹凸不平的球体,而法术本身明明是一条东方水龙。它需要用 Blender 制作龙头网格、制作一条沿着龙头路径运动的分段蛇形身体,并为两者制作水纹理,最后按这种方式重建(sim/mesh/make_dragon.py)。

之后的审查在 29 种效果中发现了同样的替代物,因此先检查这些:用贴花绘制的陨坑和作为扁平精灵图的岩石(使用 crater() + debris())、用方块充当的道具(书、剑)、只有名称所指的武器风格却没有武器(剑阵风格需要三把剑)、仅由光束构成的长枪、只有花瓣却没有花朵的花朵法术、用发光精灵图充当拳头的连击拳击。精灵图可以作为真实物体之上的一层(碎石、尘土、火花),但绝不能取代真实物体。

在宣布任何效果完成之前,先逐一审视每个可见元素,并问自己:“这究竟就是效果本身,还是仅仅拿来代替它的东西?” 列出每个替代物,并在渲染前将其替换(Blender 网格、程序化纹理、新辅助工具、新模拟器功能)。如果模拟器无法呈现效果所需的内容,就扩展模拟器。

VFX 效果不是一种资产。它是每层约 30 个数值,再加一个纹理引用。仅这一点就决定了这项技能的整体架构:效果是由你掌控的定义;连同生成的纹理和网格,它们绘制的一切也都由你掌控。

仿真器:在 Studio 外构建和评判效果

sim/ 是一个 Roblox VFX 实例的浏览器仿真器,使用 Roblox 自带的属性名称和值类型:ParticleEmitter(每项属性、翻页动画、全部四种朝向、形状、阻力、挤压、ZOffset、LightEmission 混合)、Beam(基于附件 +X/-X 的 Bezier 曲线、分段、纹理模式、滚动)、Trail、Part/MeshPart(Neon、ForceField、TextureID)、PointLight/SpotLight、Highlight、ColorCorrectionEffect、BloomEffect,以及时间线(发射 / 设置 / 补间,使用 Roblox 缓动 / 启用 / 震动)。确定性仿真:seek(t) 以 1/60 秒为步长重放,因此第 N 帧的效果始终相同。

路径内容
sim/vfxsim.js仿真核心,不依赖 DOM(可在 node 下运行),还包含 toLuau() 导出器和 fromRecipe() 导入器
sim/vfxrender.jsthree.js r128 渲染器,包含泛光和色彩校正后期处理,以及 metrics()(亮度 / 饱和度 / >250 的比例)
sim/index.html工作台页面(检查器、时间线、JSON、Luau 标签页);列出 sim/effects/index.json
sim/effects/*.json效果定义(配方编码值,CFrame = [x,y,z,rxDeg,ryDeg,rzDeg]);你的定义也放在这里
sim/effects/build_*.py参数化构建器,每种效果一个;随附三个完整示例
sim/tex/make_textures.py基础纹理,通过数学方法生成(白色 + alpha,因此 Color 可以为其着色)
sim/mesh/make_meshes.pyBlender VFX 网格(U 沿运动方向,V 横向);OBJ 可供仿真器和 Roblox 上传共用
sim/serve.py本地服务;POST /save?name=x.png 会将图像条写入 sim/out/

**快速检查流程:**运行 python sim/serve.py,在 Claude 可以操控的浏览器中打开,然后调用一次 JavaScript:await app.ready(); const s = await app.strip([3,4,6,8,12].map(f=>f/60)); await fetch('/save?name=s.png', {method:'POST', body:s.png}),并读取一张图像条以及 s.metrics。先根据数字判断时序(闪光帧、亮度峰值),再查看图像。

**代码中标记为 CALIBRATE 的行为是推断得出的,并非实测结果:**阻力(每秒减半次数)、挤压指数、Disc ShapePartial 的含义、翻页动画帧顺序、光束纹理的 V 侧、VelocityPerpendicular 的平面轴、泛光阈值比例。信任精细效果之前,先在 Studio 中逐项校准(通过 toLuau 使用相同 JSON,冻结相位后捕获并比较)。

路由:针对不同任务该读取哪个文件

任务阅读内容
品质标准、录制前门槛,以及必须能对受击做出反应的目标本文件顶部的 标准 和 能像身体一样对受击做出反应的目标;sim/effects/fxpolish.py;sim/tools/;references/HitSystem.luau
任何没有参考素材的新特效(通常情况)references/DESIGN.md(简述 -> 语料库骨架 -> 风格 -> 节拍 -> 构建 -> 评审循环)
选择外观:动漫 / 写实 / 游戏风格references/styles/STYLES.md,然后选择其中一个:anime_cel.md、realistic.md、stylised_game.md
某类别(斩击、光束、冲击、爆炸、法球、光环、终极技、元素、投射物、冲刺、拾取物)的时序 / 结构references/corpus/CATEGORIES.md 和 references/corpus/STATS.md 中的实测数据
令人难忘的瞬间references/BEATS.md
检查已构建的特效python scripts/critic_gate.py sim/effects/<x>.json [--metrics m.json] + DESIGN.md 中的盲评
添加到语料库(自行拆解参考片段)references/corpus/SCHEMA.md + references/corpus/tools/
完整构建示例sim/effects/build_explosion.py(14.5 s 的天空圆环引爆)、build_eclipse_rend.py(无参考素材的斩击)、build_riptide_lance.py(无参考素材的光束,使用道具网格)
带角色动画的特效sim/effects/fxlib.py(R6 POSES、key()、区块);特效 JSON 中的 "rig" 区块,带有 "Follow": "RightHand" 的部件会跟随身体(发射器会自动设置为 LockedToPart);{"do": "ghost"} 残影
魔法、剑,以及更多战斗动作sim/effects/fxmore.py(旋转分层 circle()、glyphs()、gather()、trail_kids()、sword()、enemy() + knock()、ring_burst、smoke、decal)和 fxlib.rig_point()(用于确定某个姿势下手的位置,适用于必须旋转的圆环)。录制任何条带或视频前,先在多个时间点渲染每个特效一次,并等待 await app.ready():条带中途才首次出现的网格或纹理不会出现在该帧中
选择纹理(适用于任何特效)生成的 LIBRARY,包含 2,000 多个原始素材(由 首次运行 构建):sim/textures/lib/ = 精灵图(14 类:圆环、斩击、符印、元素、冲击、微粒、碎片、闪光、旋涡、星星、裂纹、烟雾、形状、遮罩)、翻页动画(13 类:冲击波、冲击、火焰、烟雾、闪电、斩击、水、风、碎片、8x8 微粒、魔法、墨迹、裂纹;4x4 = 16 帧,除非名称中标有 8x8)以及光束(16 个条带系列,大多数沿 U 方向平铺)。每种形状最多有四种外观,各索引的剪影相同:cel(硬边、3 级灰度)、soft、glow(核心 + 光晕,LightEmission 1 图层)、ink(参差的笔刷效果);火焰还有 hot(预着色)。搜索:python sim/tex/lib/find_tex.py slash glow -> sim:lib/... ID;浏览 textures/lib/_sheets/*.jpg。重新生成 / 扩展:在 `tex/lib/sprites.py
特效本身就是物体(武器、书、生物、链条、岩石、冰、花)sim/mesh/make_props.py(Blender 工具包:5 种岩石、crater_rim、rock_spire、rock_fist、ice_cluster、ice_block、katana、swords、spear、axe、water_spear、wave_blade、书本部件、不同绽放阶段的花 + 茎、fist_arm、chain_link、chain_spike、phoenix body/wings/tail、portal_ring;将尺寸 + 中心点写入 meshes/props.json)和 sim/mesh/make_dragon.py;皮肤贴图位于 sim/tex/make_prop_tex.py;使用 sim/effects/fxprops.py 放置(prop() 按模型原点放置,held() 按握持点将武器放入手中,crater() 和 debris() = 真实的地面破坏,切勿只用贴花或精灵图岩石)
移动物体sim 动作:path(按弧长使用 Catmull-Rom;lag 用于蛇形物体和链条,offset/pivot、roll、spin)、hinge(下颚、翅膀:axis X/Y/Z)、toss(解除锚定,使用 Roblox 重力,反弹,沉降:碎片)、swing(绕局部枢轴旋转:书封、书页、门)。全部导出为 Luau。隐藏标签页中的渲染:使用 MessageChannel 循环等待,不要使用 setTimeout(Chrome 会在隐藏标签页中大幅降低计时器频率)
使用特定的参考视频以下步骤 1-6

高品质 VFX,每次都做到(创作路线)

目标:打造专业展示作品或动漫名场面级别的效果, 完全使用你自己的素材。采集的素材包仅用作学习参考,绝不随作品发布或采样。所有用途都由 Blender 网格和数学生成的纹理满足。请按顺序完成以下步骤;跳过步骤正是第一次尝试效果“不够惊艳”的原因。

  1. 先拆解参考作品。 下载参考作品,测量逐帧亮度和饱和度,按原始帧率剪出带编号的条带,并编写带有具体数字的阶段时间线(持续时间、增长曲线、每个阶段的颜色十六进制值,以及标明各层 Roblox 技术的图层列表)。工具:references/corpus/tools/(events.py、strip.py、measure.py)。将较大的文件(下载内容、帧、条带)保留在 LOCAL.md 中指定的研究文件夹里,绝不要放进技能目录。绝不要凭空想象并创作大型效果。
  2. 根据测量得到的配方制作纹理。 sim/tex/make_textures.py(基于工具包中的 sim/tex/vfxtex.py)实现了纹理规则:边缘硬朗,约 1 px,并配有独立光晕;2-3 个赛璐璐风格灰度层级,并带更深的轮廓;形状不对称;留白 4-11 %;逐帧图的覆盖率经过计算,以匹配测量模板(第 0 帧较小,在 t 约 0.13-0.33 时达到峰值,从约 0.4 开始收缩,在约 0.85 时消失)。新增类别 = 在该文件中新增函数。魔法阵:sim/tex/magic_circles.py(自创符文字母表、4 个可分离图层、烘焙光晕)。赛璐璐风格火焰体积:sim/blender/volume_flipbook.py(Blender,程序化生成)。
  3. 在 Blender 中制作网格(sim/mesh/make_meshes.py):U 轴沿运动方向并用于平铺,V 轴横跨网格,UV 范围为完整的 0..1。网格纹理(mesh_*)按 UV 空间布局,而不是作为径向精灵图。Roblox 不支持网格 UV 滚动:网格通过 Size / CFrame / Transparency / SurfaceAppearance.Color 属性补间来移动。
  4. 将效果构建为参数化生成器(sim/effects/build_*.py),所有数值都来自第 1 步,并按单一尺寸参数(R)缩放。图层职责:闪光、核心、主体、次级笔触、地面、残留、屏幕色调。大型招式需要摄像机、1 帧闪光、持续的白屏效果和逐渐冷却的 ColorCorrection;普通招式不需要(实测结果:4/4 个终极技需要,0/9 个普通招式需要)。将效果添加到 sim/effects/index.json,以便工作台列出它。
  5. 在模拟环境中评判效果,使用一条按阶段时间点编号的条带,以及 metrics()(根据亮度确定闪光帧数和峰值时间)。修正后重新生成条带。只有完成这一步后,才进行:
  6. Studio: node sim/studio_kit.js effects/<e>.json <out> 会写入 Studio KIT(<e>.luau + <e>.assets.json),而不是普通的 toLuau() 模块:游戏脚本可能无法写入 MeshPart.MeshId 或 SurfaceAppearance.ColorMap,因此 kit 使用 buildTemplate(assets, parent)(编辑模式,插件/命令栏:通过 CreateMeshPartAsync 创建网格、设置纹理,并用 pcall'd 包裹每次属性写入,将失败报告到 M.PROP_FAILS)+ spawn(origin, template) / play(h)(运行时 LocalScript:克隆对象,将 origin 应用于每个部件的 CFrame,将色调移入 Lighting,并在之后移除)。将纹理上传为 Image,将网格上传为 Model(从 sim/mesh/obj_to_fbx.py 导出的 FBX,绝不要上传原始 .obj;Open Cloud 密钥会从 LOCAL.md 中指定的环境变量读取),然后各插入一次模型,以读取其内部的 MeshId(kit 需要 MESH id)。 在 Studio 中操作前,运行 Lune:执行编译检查,并根据 Roblox 反射信息构建 template:lune run sim/tools/lune/compile_check.luau <kit> 和 lune run sim/tools/lune/build_check.luau <kit>(请从 aftman.toml 或 rokit.toml 中列有 Lune 的文件夹运行 lune)。 不导出:施法者骨架的动画和幽灵残影。

展示任何效果前请先自检(依据理论研究):峰值出现在生命周期前约 25 %;各图层在不同时间消失;闪光持续 1-4 帧,然后立即熄灭;每道明亮笔触下方都有暗色副本;峰值处为白色,边缘为彩色;一种主色调搭配一种低饱和度强调色;残留效果持续时间长于事件本身;转为灰度后仍能看清;所有形状都不对称;没有线性渐隐。 首个完整验证:effects/explosion_crimson.json(构建脚本 build_explosion.py),基于测量过的动漫爆炸骨架制作的 14.5 秒天空圆形引爆效果,所有资源均为自有。


采集流程

采集器是参考和原型工具,不是素材包。 它提供工具,不包含采集的效果。你需要按以下顺序构建自己的资源库:

  1. 手动将免费 VFX 素材包添加到自己的 Roblox 账户。 在 Creator Store(或 Studio 的工具箱)中找到素材包,并在登录后点击其 Get / Add to Inventory 按钮。此步骤无法自动化:insert_asset 和 InsertService:LoadAsset 都会拒绝加载账户不拥有的资源,并显示“User is not authorized to access Asset”,即使列表显示免费也一样。免费意味着可以免费获取,不代表可以免费加载。(经测量,Roblox 自带的“Particle Effects”素材包 228304820 是唯一例外:无需先获取即可加载,因此可用作测试素材包。)
  2. 将素材包放入 Studio 场景(从你的库存拖入 Workspace,可放在任意已打开的场景中)。
  3. 运行采集器(见下文的 payload.py harvest + recipe_server.py)。配方会保存到你自己的资源库文件夹中,该文件夹位于技能文件夹之外。路径见 LOCAL.md(ROBLOX_VFX_LIB)。
  4. 分类、搜索、整理并导出你自己的资源库:classify_lib.py 为其添加标签,find_vfx.py 搜索内容,audition.py 对比候选项,curate.py 复制游戏会用到的内容,emit_luau.py 写入 VFXKit ModuleScript(使用 --compact;当套件接近 Roblox 的 200,000 字符 Source 上限时使用 --data-dir;更大的文件会导致安装内容为空)。

请将你的资源库设为私有。在自己的游戏中使用免费素材包的资源符合 Creator Store 条款;提取其数值和纹理并作为自己的素材包重新分发,则是另一回事。

目录布局。 技能和资源库有意分开:资源库用于存放提取的素材包,因此必须位于技能目录之外;每个脚本都接受 --lib(或读取 ROBLOX_VFX_LIB 环境变量)。

<skill>/
  SKILL.md          this file
  LOCAL.md          your machine-specific settings (from LOCAL.template.md)
  scripts/          the harvester tooling (below)
  scripts/luau/     codec + harvest + build bodies, assembled by payload.py
  references/       VFXKit.lua, HitSystem.luau, design + style + corpus notes
  sim/              the simulator, texture and mesh generators, gate tools
  work/             scratch payloads, also served over localhost

<your library folder>/     path set in LOCAL.md, outside the skill folder
  harvested/        raw intake from the harvester. Look at it, then promote.
  recipes/*.json    the recipes you kept
  compositions/     optional: timelines you author over your recipes
  index.json        search index, built by classify_lib.py
  .cache/           texture thumbnails + tag caches, all regenerable

入口点,均支持 --lib:

命令功能
find_vfx.py --lib DIR <words> --layered按效果的实际内容搜索
payload.py build --recipe F --target PATH --out work/b.lua组装构建负载
recipe_server.py接收采集内容,并通过 localhost 提供 work/*.lua
verify_vfx.py --lib DIR --quiet检查缺陷;--self-test 可验证检查确实能发现缺陷
classify_lib.py --lib DIR(重新)生成 index.json;--sheet ring 可审查标签
payload.py harvest --root PATH --out work/h.lua从 Studio 采集素材包
assemble_kit.py、contact_grid.py、fetch_tex_hires.py从多个配方构建游戏套件、在一张图上查看整个套件、获取翻页动画的高分辨率缩略图

操作 Studio 之前,请确认 Studio 桥接确实处于活动状态。插件已连接和插件正常工作是两回事。使用 Roblox 官方 Studio MCP(推荐)时,运行一个简单的 execute_luau(return 1)并执行 screen_capture;本文中提到的 capture_screenshot,在官方服务中对应 screen_capture。使用较旧的第三方 robloxstudio-mcp 服务时:

curl -s localhost:48741/health

只有 "pluginConnected": true 才是有意义的信号。如果显示正常,但工具仍然超时,请参阅下文的 “MCP 已连接,但每次调用都超时”。

要从资源库构建并评估效果,请参阅 “预览采集的配方”。要向资源库添加素材包,请参阅 “采集素材包”。


收集器基于的核心理念

不要生成粒子参数,而要检索它们。

(对于已发布的高质量作品,应采用上文的创作路线:模拟器让生成的参数可离线验证,而生成的纹理和网格则让每项资产都归你所有。检索仍是原型和占位内容的快捷方式。)

从文本提示词生成优质发射器,无法离线验证,因为 Roblox 之外没有任何东西能模拟 ParticleEmitter。检索人类艺术家已经调校好的效果,则能轻松验证,并且立即得到正确结果。这与 T1/T2 的逻辑相同:相较于生成 SFX,Creator Store 音频才是正确选择。

让它成为一项技能而非复制粘贴的,是上层这一层:

free VFX pack in Studio
   -> harvest.lua (explicit property walk)  -> recipe JSON (yours, portable)
   -> tune live in Studio, harvest back     -> recipe JSON (updated)
   -> emit_luau.py                          -> VFXKit ModuleScript
   -> VFXKit.build(name, part, {scale, tint, lifetime_mul, rate_mul, offset})

检索能给你一个爆炸效果。变换层则能让你得到“那个爆炸效果,缩放至 0.6,染成绿色,并在此事件发生时于此附件处发射”。

这项技能是收集器,而非收集成果。 在自己的游戏中使用免费资源包资产,符合 Creator Store 条款。提取其中的数值并将其作为自己的资源包重新发布,则是另一回事;纹理显然属于他人。因此,这项技能提供工具,每位用户都从自己账户中的资源包构建自己的素材库。


请先阅读:15 个容易踩坑的地方

1. MCP 的通用属性读取器只能看到 ParticleEmitter 的 18%。 get_instance_properties 和 get_class_info 都只返回相同的 9 个通用字段:Size、Rotation、Transparency、Color、Enabled、Brightness、Name/ClassName/Parent。缺少:Rate、Lifetime、Speed、SpreadAngle、Drag、Acceleration、Texture、LightEmission、EmissionDirection、Shape、ZOffset、Squash、FlipbookMode、LockedToPart 等。实测结果:31 个真实属性中只能读取到 6 个。通过这些工具收集时,每个配方看起来都已填充,实际上却有四分之三为空,而且不会发出任何提示。务必使用 execute_luau 和显式属性遍历来收集(scripts/luau/harvest.lua)。verify_vfx.py 会因配方属性集过小而失败,原因正是如此。

2. Roblox 自带的粒子纹理只有 5 种,而且有一条看似显而易见的路径是陷阱。 在实际收集一个资源包之前,本文曾写着“只有 3 种”。第一轮只探测了 textures/particles/*.dds,没有检查 textures/ 本身,因此有两条有效路径被错误地判定为不存在。以下是逐一渲染后的实测结果:

路径外观适用场景
textures/particles/sparkles_main.dds柔和的 8 角星余烬、微粒、魔法效果
textures/particles/fire_main.dds带噪点的橙色火焰火焰主体、爆炸
textures/particles/smoke_main.dds柔和的圆形烟团烟雾、尘土、发光核心、光环
textures/explosion.png蓬松发亮的爆炸云爆炸、冲击、卡通火焰
textures/sparkle.png边缘较暗的锐利 8 角星尖锐火花、受击闪光

rbxasset://textures/smoke.png 渲染结果为空——而 Roblox 自家的“Particle Effects”资源包(228304820)中有一个 Smoke 图层直接指向它。因此,第一方资源包里竟然包含一个永久不可见的发射器。textures/fire.png 和 textures/particle.png 也是如此。除了上述 3 种纹理外,textures/particles/ 下的所有纹理(explosion01_*、light_diffuse、smoke_dust、burst、slash、glow、star、sparkles_bright)也都无法使用。

错误的纹理路径会导致发射器不可见,而不是抛出异常。verify_vfx.py 会明确列出这 3 条实测无效的路径,而不会将它们报告为一般未知路径,因为当路径来自 Roblox 资源包时,“不在自带纹理集合中”看起来会像误报。

3. Model 或 Folder 下的 Attachment 没有世界坐标。 构建目标必须是 BasePart。将 Attachment 放在其他任何位置,发射器都会在世界原点静默构建:效果“能用”,只是离所属对象十万八千里。VFXKit.build 会对此断言报错。 4. 附着在 Attachment 上的球形发射器会从接近单位大小的体积中生成粒子。 它没有发射器大小属性。对于角色来说,这意味着每个粒子出生时都在 躯干 内部,会被躯干遮挡。若要环绕角色,粒子要么必须足够大,能与角色 轮廓重叠;要么必须具有足够的向外速度,才能逃离躯干。制作光环时,两者都要用。

5. capture_screenshot 需要 Studio 窗口处于前台,而不只是 未最小化。 最小化的窗口会失败,并显示 “确保 Studio 视口可见…… 已知 Roblox 错误:如果视口渲染为纯色,捕获可能会失败。” 这条消息把原因 归咎于视口颜色。在检查 IsIconic 之前,我尝试了三个错误假设 (背景板大小、TimeScale、过亮的 PointLight),白费了不少工夫。

IsIconic == false 还不够——恢复但处于后台的窗口仍会超时。可靠的做法是最大化并置于前台:

[W]::ShowWindow($h, 3)          # SW_MAXIMIZE
[W]::SetForegroundWindow($h)

纯色导致失败也确实存在,所以画面中要保留一些天空或地面,不要让单一平面背景占满画面。

6. 只要脚本发生任何让出,execute_luau 就会静默丢弃所有 print 输出。 不只是尾随等待之后的输出:如果脚本的 第一条 语句就是会让出的调用(InsertService:LoadAsset、GetKeyframeSequenceAsync), 它会返回 success: true,但输出数组为空;在返回很久之前执行的 print 也会丢失。“缩短等待时间”并无帮助,因为让出本身就是工作内容。

解决方法是:不要用返回通道处理任何会让出的操作;改用 task.spawn 执行工作,将进度写入 StringValue,然后在第二次调用 execute_luau 时将其读回。

local sv = Instance.new("StringValue"); sv.Name = "__probe"; sv.Parent = game.ReplicatedStorage
task.spawn(function()
    ...                       -- yields freely
    sv.Value = "DONE :: " .. result
end)
return "spawned"              -- returns immediately, output intact

因为每次迭代都会写入进度,即使脚本中途崩溃也能保留进度,而 print 做不到这一点。

7. Rate > 0 的爆发层会在爆发的同时持续发射。 这是最常见的创作失误, 看起来就像“特效永远不会停止”。VFXKit.build 会将任何发射模式为 burst 的层的 Rate 强制设为 0,验证器也会因此报错。

8. 深色背景上的深色烟雾看不见,这是布景造成的假象, 不是特效本身有问题。 修改参数前,务必在明暗两种 表面上检查每个特效。有个烟雾层曾差点被当作“无法运行”而删除,实际上它在地面上显示得非常清楚。

9. 真正的特效包会将发射器直接设为 Model 的子对象,因此完全不会渲染。 在 Roblox 自己的 228304820 上测得:Model 'Particles' 有三个直接子对象,都是 ParticleEmitter——没有 Attachment,也没有任何 BasePart。Model 下的 ParticleEmitter 没有世界坐标位置,因此完全不会绘制。 该特效包只是一个零件集合,供人拖到其他对象上使用,并不是可直接运行的特效。因此,插入的特效包在视口中看起来毫无反应,并不能说明其中的配方有问题;应先提取,再根据重建后的效果进行判断。 harvest.lua 会记录 parent_class 和 renders_in_place: false,验证器只会发出警告而不会报错,因为这些参数本身完全有效。

10. 在编辑模式下设置 CurrentCamera.CFrame 不会生效。 Studio 自带的 摄像机控制器会在 execute_luau 调用之间覆盖该设置,因此固定在世界坐标位置的装置往往会在截图时超出画面。应反其道而行:读取 CurrentCamera.CFrame,然后将装置放在 cf.Position + cf.LookVector * d 处,并沿 cf.RightVector 排列。 这样屏幕上的顺序就会与放置顺序一致,从左到右排列,从而确保带标签的联系表可信。


11. GetFullName() 不唯一,将它用作分组键会悄无声息地丢失配方。同一个 Folder 下,十个同级 Part 都叫 Part,返回的字符串完全相同。以它为键分组,会把互不相关的效果合并成一个巨大的配方,并且写入时还会相互覆盖。在一个真实场景中测得:583 个配方中有 366 个消失了,而幸存下来的配方中还包括 40 层和 55 层的“效果”。应以实例本身作为分组键,然后在记录路径末尾加上 #n 后缀来消除歧义。作为第二道防线,接收端会拒绝覆盖 source_path 不同的文件。

12. PostAsync 会拒绝过大的请求体,而备用打印内容不可见。包含 1447 个配方的包编码后约为 2MB,POST 会抛出异常。pcall 捕获异常后,代码会改为分块打印;但如果采集任务运行在 task.spawn 中(必须如此,见陷阱 6),这些打印也会被丢弃。最终,该包报告“已采集 1447 个”,实际却什么也没写入。每批 POST 约 40 个,并且返回实际写入磁盘的数量,而不是扫描到的数量。采集器报告扫描量而非保存量,从根本上就是在撒谎。

13. 大多数包容器都叫 Part、Model 或某个数字。在一个包含 3100 个发射器的真实场景中,61% 的发射器都是如此。以最近的容器名称为配方命名,会生成数百个叫 Part.json 的文件——文件完整且经过验证,却无法搜索,这跟没有采集到没什么区别。越过通用名称,找到最近一个人类实际命名的祖先,但绝不能一直向上到采集根节点,否则整个包会塌缩成一个配方。

14. 所谓的“VFX 包”有一半其实是纹理库。其结构为:Part '78' -> Attachment -> ParticleEmitter 'Images/Lightning',每个发射器对应一个效果,数量达数百个。单层配方在这种情况下是正确的,并非分组错误——而唯一具有实际意义的名称在图层上,不在容器上。因此,当容器名称没有意义且配方只有一个图层时,应使用图层名称命名(Images/Lightning -> Lightning);如果图层也叫 Texture,则回退到纹理资源 ID,至少它是唯一的,并且能对应回图像。这样一来,1660 个无法查找的文件变成了 1124 个以人类可读名称命名的文件。

**15. 采集到的效果针对其采集时所用的场景进行了调校,而 LightEmission = 1 就是陷阱。**它会让 ParticleEmitter 四边形呈现加法混合:把自身叠加到后方内容上,而不是覆盖后方内容。大多数包都是在深色场景下拍摄的,看起来像发光。把同样的效果放到浅色环境中——白色石头、雪地、阴天的天空——每个四边形一绘制,整个区域就会饱和成纯白,因为背景本身已经接近范围上限。

在一张浅色竞技场地图中测得:终结技命中时,画面有 44% 的区域完全饱和,持续五分之一秒;就在玩家需要看清命中的瞬间,双方角色都被一片白光遮住了。有两种显而易见的修复方法没有效果,而且两者都没抓住问题的关键:

修改完全饱和的画面比例
按采集结果原样保留44.1%
闪光和核心四边形缩小 2.7 倍44.1%
火花数量从 44 减至 24,缩小尺寸~40%
脉冲灯从 9/14 调至 3.0/4.5,范围减半32.5%
LightEmission 从 1 调至 0.35-0.5515.4%

缩小四边形并没有帮助,因为饱和区域由覆盖范围决定,而不是由任何单个四边形的尺寸决定——而覆盖范围几乎没有变化。

由此得出两条规则:

  • 根据被洗白的内容判断洗白效果。 地面均匀变白、纹理消失,这是光照或混合模式造成的。四边形爆发则不同:它有边缘和尖角。更改数值前,先以全分辨率查看峰值帧,并测量饱和度(像素值 > 250 的像素占比),不要只凭亮度估算——一个浅色竞技场静止时的占比是 4%,峰值时是 44%,两者之间的差异就是诊断的全部依据。
  • 对照同一文件中的脉冲灯光与恒定灯光。 一个构建版本中,脉冲峰值为 9 和 14,恒定辉光却为 2.2 和 3.0。这个差距并非刻意设计,而是直接来自采集结果。超过约 4 的任何数值单独就足以洗白浅色场景。

16. 采集到的 LAYER OFFSET 可能是结构的一部分,而且引擎会对其进行缩放。 每个图层都会记录其发射器附着点在资源包中的位置。大多数资源包中的值为零或仅作小幅调整;对于 BEAM 资源包来说,它就是光束的远端。Lava-01 的图层 #1(FireSpecs1)位于 6.25 studs 之外,而 Lighting-01 的 Lightning 图层则位于上方 9.9 studs 处。引擎会将图层偏移乘以元素的 scale,因此在深度为 4 的缩放下,每道火流中的余烬都落在距离其自身效果 15-27 studs 的位置——一团脱离效果的火星浮在施法者上方,看起来像是另一个 Bug,但实际上不是。

有两种解决办法,都已采用:如果资源包中有偏移量为零的同款图层,就选择它(Lava-01 的 #6 与 #1 完全相同,偏移量为 0,逐字节一致);或者在配方元素上设置 centred: true,让每个图层都基于其锚点构建,并忽略采集到的位置。该选项需要显式启用,因此不会改变现有组合。对于任何手动选择图层的组合集,值得编写一个小型检查器:如果所选图层偏离锚点超过 0.05 studs,就让检查失败。

17. 圆环会消耗 segments 个 Neon 部件,因此应根据半径设置分段数。 圆环由一圈部件构成,而不是单个网格。按深度设置分段数,会让一个半径为 1.6 studs 的治疗圆环与一个半径为 15 studs 的冲击波拥有相同的 66 个部件;每拍都生成一组新圆环的恢复治疗效果,则在一次施放中创建了 2473 个实例。24 + 2.4 * radius(上限为 64)能让各种尺寸都保持平滑;重复播放的圆环应当重新排入队列(在同一 ID 上显示/调整半径/淡出),而不是重新构建——时间线会移除已结束的轨道,因此后续提示可以顺利接管,只要同一圆环的两次使用不重叠即可。

18. 如果某个组合用 Parts 绘制效果,那它就没有使用这项技能。 引擎可以构建 Neon 圆环、部件组和由部件分段构成的闪电,但人们很容易用这种方式构建效果的“结构”(冲击波圆环、柱体、魔法阵、闪电、墙壁),再在上面点缀采集到的发射器。这是在生成,而非检索。一个大型效果集就这么做了:使用了 1813 个库配方中的 12 个,包含 1696 个由部件构建的元素,最终全部在评审中被否决。重建时,有人真正查看后发现,所有这些角色都有对应的采集效果(扁平冲击波纹理、魔法阵、龙卷风环、冰霜外壳、光环装置、光束飘带、卡通闪电精灵图)。由此总结出以下规则:

  • 先调查,再构建:按资源包和名称列出库中的效果(index.json 以 FILENAME 为键——按显示名称连接会漏掉整个资源包);针对每个角色并排试听候选效果,统一到相同尺寸(资源包中的效果原本有的仅点大小,有的则达 35 studs),并将选择作为角色映射写入数据。
  • 在多个阶段检查候选效果。 Realistic-Explosion-01 会先闪白三帧,随后才变成橙色火球;如果在 0.3s 时进行判断,它就会被用于每个元素的收尾阶段,并让火球出现在水系光环上。
  • 采集到的地面波是一个实心圆盘,而圆环只是轮廓:相同半径会覆盖数倍的屏幕面积。尺寸应约为 1.3r,而不是 2r。
  • 计数并设置门槛:统计每种效果中的采集元素与部件数量;如果有任何可见部件既不是不可见载体,也不是角色的复制体,就让构建失败。 20. 色调无法重新着色自带颜色的纹理。 色调会与 Color 属性相乘;纹理随后再与其相乘。WaterBurst 的 ToonLiquidColor 图集预先烘焙成蓝色,因此“金色蜂蜜”渲染出来发暗偏绿, “粉色蛋糕”仍然是蓝色,而它的灰色 ToonLiquidAlpha 对应图则能完美着色。 如果某层采集到的 Color 是 BLACK(Portal-01 的 Portal),则完全无法着色。 选择色调前,请先测量每个保留层的纹理(.cache/tex PNG),并且重新着色时只保留灰色图层。

19. Roblox 脚本的 Source 必须少于 200,000 个字符。 赋值过长的字符串会抛出 Unable to assign property Source。包含许多采集配方的已发布效果集很快就会超出限制(54 个配方:356K)。将数据拆分到存放 JSON 字符串的子 ModuleScripts 中,并按需延迟解码。

元素类型(engine.lua)
类型 / 字段说明
beam一个采集的 Beam 图层(recipe、layer),连接在 from/to 锚点(+偏移)或移动部件(parent/to_parent)之间;width_mul、tint。此前完全无法使用采集的 Beam。
配方内的 Beam 图层根据采集器记录的 endpoints 偏移自动连接(光环装置:地面发射器 + 上升光束)。
chain一个采集配方,在两个锚点之间的 points 个宿主上串联,jitter——由采集到的效果自身绘制的线(闪电)。
centred配方字段:在锚点上构建每个图层,忽略采集到的图层偏移(陷阱 16)。
rate_mul、rate_cap按采集效果自身包速率的倍数播放效果流(突发采集后仍保留 harvested_rate),并设上限。

组合(可选,由你编写)

一个配方是在一个部件上的一个采集效果。组合是由你自己的多个配方和简单几何图形组成的时间线, 可用于“光球在手中成形、膨胀、脉动、释放”等效果。该技能附带引擎,不附带任何组合: 你需要根据自己库中的配方自行编写 JSON。

{
  "name": "my_effect",
  "title": "My Effect",                  // on-screen button label (emit_scenes.py)
  "duration": 3.0,
  "elements": [
    {"id": "core", "kind": "recipe", "recipe": "MyRecipe",   // a file name in your library
     "anchor": "A_Hand", "scale": 0.8, "layers": ["Core", "#3"]},
    {"id": "ring", "kind": "ring", "anchor": "A_Ground", "radius": 2, "segments": 32}
  ],
  "timeline": [
    {"t": 0.0, "id": "core", "act": "enable", "on": true},
    {"t": 1.2, "id": "ring", "act": "radius", "from": 1, "to": 12, "time": 0.6}
  ],
  "camera": {"look": "A_Chest", "offset": [-12, 6, -18], "fov": 50}   // edit mode only
}
  • 元素类型: recipe、beam、chain、ring、part、light、group、 bolt、trail、clone/model(参见下方类型表和 engine.lua)。
  • 提示动作: emit、enable、show、hide、destroy、regen、warp、 scale、radius、fade、light、spin、fly、spiral、burst、 stagger、move。提示包含 t、id、act,以及动画相关的 time / from / to / style(缓动名称)。
  • 锚点可以是名称(A_Ground、A_Feet、A_Waist、A_Chest、A_Head、 A_Air、A_Sky、A_Hand、A_Far),也可以是字面量 [x, y, z]。在游戏模式下, 字面量表示角色空间:x 向右,y 向上,-z 向前,原点位于角色脚下的地面点。编辑阶段 (stage.lua)会构建一个假人和相同的锚点,因此你在编辑模式下评估的效果就是运行时构建的效果。
  • 编辑模式循环: compose.py my_effect.json --lib <lib> --freeze 0.45 --out work/s.lua,运行载荷、截图,然后在其他阶段重复操作。 stage.lua 会移动 Baseplate 和出生点,因此只能在临时场景中运行。 compose.py my_effect.json --self-test 可验证组合检查确实能够失败。
  • 发布: emit_scenes.py --lib <lib> --comps <your compositions folder> --out-dir work 会写入一个 VFXScenes ModuleScript(包含组合及其去重后的配方),以及一个 VFXDemo LocalScript,为每个组合提供一个按键和按钮(前八个组合使用按键 1-8)。
  • 当心陷阱 18:使用 Parts 绘制效果的组合是在生成效果,而不是检索效果。用引擎的部件类型进行衔接,并使用采集配方呈现可见效果。

规模:广泛采集,精简交付

真正的采集成果比你预想的还要多。一个下午从 13 个免费素材包中采集到 1813 个配方 / 2238 个图层,全部通过验证,只有一个确实失败。

不要输出整个素材库。 emit_luau.py 会将每个配方内联到一个 ModuleScript 中;按这个规模计算,生成的 Luau 预计有 4.7MB,这不是 适合放进游戏的东西。素材库是一个供检索的归档,而 VFXKit 是 你挑选的子集。挑出效果实际需要的少数几个,再进行输出。

大型采集成果通常会呈现这样的分布:大量单图层纹理条目,少量真正的多图层效果(这里有 1813 个中的 153 个),以及少数 Beam。多图层效果才是需要精心制作的部分;其余大量条目只是 查找表。

验证循环

离线环境无法模拟粒子,因此验证关卡就是 Studio,且处于编辑 模式(无需 Play,已确认可用)。

冻结特定相位的捕获技术让这一流程切实可行。 爆发效果持续不到一秒, 截图效果全凭运气。取而代之的是:发射效果,用 task.wait 等待想要检查的准确时刻,然后将每个发射器的 TimeScale = 0。 模拟便会冻结,并保持冻结状态,直到将其恢复为 1,因此每次捕获都能落在指定的相位。

for _, e in ipairs(handle.emitters) do e.TimeScale = 1; e:Clear() end
handle.emit(1.0)
task.wait(0.22)                       -- the phase you want to judge
for _, e in ipairs(handle.emitters) do e.TimeScale = 0 end
-- now capture_screenshot

至少检查早期相位(闪光是否明显)、中期相位(形状是否正确)和晚期相位(是否有尾迹,还是会直接停止)。

联系表用于给候选项排序,最终由你的眼睛判断。 verify_vfx.py 中没有任何指标能评判效果是否好看,也没有指标能做到这一点。它能捕捉的是静态图无法展现的缺陷类型:属性集被截断、发射器在整个生命周期中都透明、爆发图层变成持续发射、纹理路径渲染为空、序列没有覆盖 t=0..1,以及可移植性标记与纹理引用相矛盾。

对于采集到的配方,它还会发出两类警告,它们代表待处理事项,而非缺陷:发射模式是推断得出的而非原始记录的图层,以及源素材包将父级设在无法渲染的位置的图层。这两类情况都只能由人工判定。

python scripts/verify_vfx.py --lib DIR --quiet     # your harvested library
python scripts/verify_vfx.py work/harvested/SomePack.json   # one file
python scripts/verify_vfx.py --self-test  # prove the checks can fail (10/10, no library needed)

文件

路径用途
scripts/luau/codec.lua在 Roblox 值和 JSON 之间进行类型化往返转换。每个载荷都会在前面添加一份定义。
scripts/luau/harvest.lua显式遍历容器的属性。读取发射器的唯一正确方式。
scripts/luau/build.lua在 Studio 中将配方构建为实时实例,并应用变换。
scripts/payload.py将 codec、主体和替换项组装为一个载荷。--sink 用于设置 POST 目标。
scripts/recipe_server.py本地主机接收端和载荷服务。Harvest 会 POST 到这里;GET /payload/<f>.lua 会从 work/ 提供文件,以供 loadstring 使用。
<library>/harvested/原始收集目录(未设置 library 路径时则为 work/harvested/)。Harvest 的结果会存放在这里,不会直接进入 recipes/。检查后再手动移入。
scripts/emit_luau.py将配方 JSON(--lib DIR)转换为可发布的 VFXKit ModuleScript。
scripts/verify_vfx.py检查项以及破坏性自测。使用 --lib 指定自己的库,使用 --quiet 仅显示失败项。
scripts/classify_lib.py为收集的库添加标签,以便搜索:获取纹理,从值中读取运动信息、从像素中读取形状,并写入 index.json。使用 --sheet ring 进行审查。
scripts/find_vfx.py按特效的实际效果搜索索引。支持 --layered、--portable、--tag、--json。
references/VFXKit.lua由 emit_luau.py 生成。技能包中的副本是空工具包;你自己的副本会保存生成的配方。放入 ReplicatedStorage。
references/CharacterVFX.server.lua完整示例:角色生成时播放光环,落地时播放冲击爆发效果。
scripts/compose.py、scripts/emit_scenes.py、scripts/luau/engine.lua、scene.lua、runtime.lua、stage.lua、demo_controller.lua可选的合成引擎:在你自行编写的配方上创建时间线(见下文的 合成)。
scripts/audition.py在任意场景中并排展示同一角色的候选效果(--at x,y,z,清空的 Workspace.__VFXStage),将尺寸统一,并从左到右统计槽位条。绝不会运行 stage.lua。
scripts/curate.py按角色将游戏使用的配方复制到游戏中:`NAME=STEM
LOCAL.md仅适用于你这台机器的路径。切勿共享。

收集资源包

状态:已在一个真实的第三方资源包上端到端运行收集器, 往返转换无损。 在 Roblox 自带的 “Particle Effects”(228304820)上进行测量:收集 -> JSON -> 重建,然后 逐项比较重建后的发射器与原始发射器的属性差异——在 31 个属性 × 3 层中, 84 个值全部完全一致;并排的冻结相位捕获也无法区分。剩下未知的是资源包的 形态,而非保真度。

将资源包获取到自己名下

search_assets 是死路。别在它上面浪费时间。 通过 MCP 调用时,它通常会对 /toolbox-service/v2/assets:search 返回

{"errors":[{"code":0,"message":"Scope not authorized."}]}   # HTTP 403

重命名 API 密钥变量也无济于事。需要一把拥有 Creator Store 权限范围的新 Open Cloud 密钥。

你不需要这把密钥。 Studio 自带工具箱使用的端点完全不需要身份验证:

curl "https://apis.roblox.com/toolbox-service/v1/marketplace/10?limit=20&keyword=vfx%20pack&sortType=3"
curl "https://apis.roblox.com/toolbox-service/v1/items/details?assetIds=ID1,ID2"

marketplace/10 会返回资源 ID(10 = Model)和 nextPageCursor;items/details 会返回名称、创作者、createdUtc、categoryPath、fiatProduct.isFree 和 capabilities.shouldSandbox。这些信息就是 search_assets 能提供的全部内容。

真正的阻碍是插入,而不是发现。 对于尚未归你所有的 Creator Store 资源,insert_asset 和 InsertService:LoadAsset 都会失败,并提示 "User is not authorized to access Asset"——即使 isFree: true 且 purchasable: true 也是如此。免费不代表可以加载。你必须先通过 Creator Store 或 Studio 工具箱,亲自手动将资源添加到自己的账户中。没有脚本能替你完成这一步。

在 33 个免费 VFX 资源包中进行测试后发现:只有一个资源包无需先获取就能加载——228304820,由 Roblox 所有,也是整个测试中唯一 shouldSandbox: false 的结果。按 shouldSandbox 对候选项排序,并假设所有值为 true 的资源都需要先获取。 因此,真正开始收集前,坦诚地说第一步就是:将资源包添加到自己的账户中,然后再收集。

执行流程
  1. 启动接收服务。 这是实际的输出路径——分块打印适用于三个配方,处理两百个时就会出问题,而被截断的配方看起来和有效配方一模一样。

    python scripts/recipe_server.py          # lands in <ROBLOX_VFX_LIB>/harvested
    

    它会将收集结果保存到 harvested/,而不是你精心整理的 recipes/ 文件夹,这是有意为之。若将接收服务指向 recipes/,三十个未经查看的配方就会和你挑选的配方混在一起。请手动移入。

  2. 组装载荷。

    python scripts/payload.py harvest --root Workspace.SomePack --out work/h.lua
    
  3. 通过 loadstring 运行,不要直接粘贴。 插件上下文中确实可以使用 loadstring(经实测),而接收服务会通过 localhost 提供 work/*.lua,因此载荷大小不再构成限制:

    local src = game:GetService("HttpService"):GetAsync("http://127.0.0.1:48752/payload/h.lua")
    local fn, err = loadstring(src)
    if not fn then print("COMPILE_ERROR " .. tostring(err)) return end
    return fn()
    

    预期会看到 HARVEST_POSTED recipes=N len=M。如果接收服务未运行,脚本会退回到分块打印并说明原因,因此忘记启动接收服务只会变慢,不会悄无声息地截断内容。

  4. 验证,然后整理,再移入正式目录。

    python scripts/verify_vfx.py --lib "<your library>/harvested" --quiet
    
  5. 将保留的配方从 harvested/ 移到 recipes/,运行 classify_lib.py --lib <your library>,然后运行 emit_luau.py --lib <your library>/recipes(如果只需要适用于游戏规模的子集,可以先运行 curate.py)。

收集时要针对容器层级,而不是单个发射器。 一个真实特效由三到四层组成(明亮且快速的核心、较慢的主体、高阻力火花、短暂的光效)。索引单个发射器只会得到一堆零件,无法知道它们原本属于什么特效。

收集器会推断什么,以及为什么你必须检查

资源包不记录任何发射元数据。它提供的发射器均为 Enabled = false,但 Rate 有效,这意味着“短暂开启我”——除名称外就是一次性发射,但数据中没有记录持续时间。因此,harvest.lua 会进行猜测:count = clamp(round(Rate * 0.1), 1, 250),并将该层标记为 "inferred": true,同时保留 burst_window 和 harvested_rate,确保没有信息被悄悄丢弃。验证器会对每个推断出的层发出警告。

将这些警告视为整理队列。在 228304820 中,Rate = 1000 被转换为 count = 100;艺术家原本想要 40 还是 400,无法从资源本身还原,只能通过查看效果来判断。

会破坏分组的包结构

containerOf 会向上查找最近的 Model 或 BasePart。已测量和预测到的问题:

包结构出了什么问题如何处理
Emitter 直接以 Model 为父级(在 228304820 上已测量)分组正确,但偏移量读出来全是 [0,0,0],不会在原位渲染这是正确行为,不是 bug。系统会记录 parent_class 和 renders_in_place;重建时会将它们放到 BasePart 上,效果会原样呈现
一个 Model 下有十个互不相关的效果全部合并成一个巨型配方按拥有同级对象的最近祖先分组;或者当一个 Part 上有多个互不相关的效果时,按 Attachment 拆分
Emitter 直接以 Part 为父级偏移量读出来全是 [0,0,0]这是正确行为:以 Part 为父级的 Emitter 没有 Attachment 偏移量
嵌套 Model(效果位于道具内,且道具位于套件内)配方使用了错误祖先的名称使用最外层 Model 的名称,且该 Model 的所有后代都属于 VFX 类

只有第一行的问题在实际中出现过。不要猜自己遇到的是哪种情况。导出树结构并检查(使用带 classFilter 的 get_descendants,或者用 Luau 遍历并打印 ClassName/Name/Parent.ClassName)。输入内容完全可见。

Beam 和 Trail 需要结构信息,而不只是属性值。 Attachment0/Attachment1 是实例引用,记录为 {"Ref": "<full name>"}。重建时需要在目标上创建两个 Attachment,并将 Beam 指向它们,因此 Beam 配方需要一个 endpoints 块,用于指定两个偏移量。由于目前还没有采集到 Beam,这个块尚不存在。等第一个 Beam 出现时再添加,不要提前设计。

在导入时判断纹理来源,并做好大多数包无法通过的准备。 优质包会附带自己上传的纹理,因此大多数采集到的配方会是 texture_portable: false。这对在自己的游戏中使用没有问题,但会限制你分享这个库。尽早按此属性分类,方便找到可移植的配方。注意,texture_portable: true 仅表示 “每个引用都是 rbxasset:// 路径”,不表示这些路径有效。228304820 被采集为可移植,但它的两个纹理中有一个已失效。

筛选整理这一步无法机械完成。 采集三十个配方,得到的只是三十个配方,并不代表它们都很好。这里没有任何机制可以判断品味。为每个配方生成预览,逐一查看,并在建立索引前删除劣质内容。

如果免费包的质量达不到要求,这项技能也不会失去价值。 它会转而用于按照下文的配方格式手动编写和应用配方。采集是加速手段,不是基础。


MCP 已连接,但每次调用都会超时

如果没遇到过,这个问题会浪费你一个小时。/health 显示 pluginConnected: true,但每个工具调用都返回 "Studio plugin connection timeout"。

适用于第三方 robloxstudio-mcp 服务:有两个副本正在运行。 第二个副本没有按预期进入代理模式:listenWithRetry(app, host, 48741, 5) 在 48742 上成功,因此它认为自己是主服务,且不会提升为主服务。而 Studio 插件只会轮询 48741。你的工具连接到一个服务,插件连接到另一个服务,但两边都没有报告任何错误。

curl -s localhost:48741/health; curl -s localhost:48742/health

两个回复都为 "status":"ok",但只有一个显示 pluginConnected: true,这就是问题特征。然后:

  1. 终止占用插件所用端口的服务,也就是你的工具没有连接的那个服务。终止你连接的服务会结束当前会话的 MCP。

  2. 从 48741 到你的端口建立原始 TCP 转发即可,因为插件会重新连接到 48741 上任何响应的服务。

  3. 插件仍会显示为未连接。 GET /poll 不会注册实例,只有 POST /ready 会注册;而插件只会在启动时发送一次该请求。请使用其轮询请求携带的 instanceId 手动注册:

    curl -X POST http://localhost:<yourport>/ready \
      -H "Content-Type: application/json" \
      -d '{"instanceId":"<from the /poll query string>","role":"edit"}'
    

TCP 连接处于 ESTABLISHED 状态且 /poll 返回 200,看起来都很正常,但桥接实际并未工作。/health 中的 pluginConnected 是唯一有意义的信号。

预览采集到的配方

从库中判断单个配方的流程。这里的所有内容都有实测依据; 文中提到的陷阱对应上文中的编号。

  1. 启动 Sink,组装载荷。 Sink 也会通过 localhost 提供载荷。

    python scripts/recipe_server.py --out "<lib>/harvested"
    
    python scripts/payload.py build --recipe "<lib>/recipes/X.json" --target "Workspace.__VFXTarget" --out work/b.lua
    
  2. 相对于摄像机放置目标。 在编辑模式下设置 CurrentCamera.CFrame 不会生效(陷阱 14)。先读取摄像机,再将部件放在 cf.Position + cf.LookVector * d 处。目标必须是 BasePart(陷阱 3)。 在它后方放置中灰色背景,但画面中要保留一些天空或地面,否则会因纯色 故障导致捕获失败(陷阱 5)。

  3. **通过 loadstring 运行,**不要直接粘贴:

    local fn = loadstring(game:GetService("HttpService"):GetAsync("http://127.0.0.1:48752/payload/b.lua"))
    return fn()
    
  4. 先发射,再冻结相位,最后捕获。 使用配方自带的发射数量, 不要自行编造一个数字:

    for _, e in ipairs(emitters) do e.TimeScale = 1; e:Clear() end
    for _, e in ipairs(emitters) do e:Emit(e:GetAttribute("EmitCount") or 20) end
    task.wait(0.22)                                   -- the phase you want
    for _, e in ipairs(emitters) do e.TimeScale = 0 end
    

    在早期/中期/后期采样。先最大化 Studio 窗口并将其置于前台 (陷阱 5)——IsIconic == false 还不够。

除了“看起来是否好看”之外,还要检查:

  • 发射数量是否合理? 几乎所有采集到的配方都是 emit-inferred: 资源包没有记录爆发时长,因此数量是根据 Rate * 0.1 猜测的, harvested_rate 则保留在旁。一份忠实的配方若使用了编造的数量, 密度可能高出五倍,却仍能通过所有检查。**这是采集到的效果看起来不对的 最可能原因,**只有你的眼睛才能发现。
  • 在浅色和深色表面上是否都清晰可辨?(陷阱 8)
  • 它会停止吗? Rate > 0 的爆发层会永远持续发射(陷阱 7)。

编辑配方 JSON 并重新构建即可修正——JSON 是唯一真实来源, emit_luau.py 也会读取相同的文件,因此你调试的内容与最终发布的内容 不会不一致。

再次查找任意内容:先分类,再搜索

没有这一步,采集到的库就无法使用;这绝非小问题。 名称取决于资源包作者随手输入的内容。在一个真实的、含 1813 个配方的 库中进行测量后发现,只有 29% 的名称包含任何有用信息——其余都是 Part、78 或纹理 ID。采集过程完整、验证无误,却无法搜索,这和没有 采集到它没有区别。

python scripts/classify_lib.py --lib "~/Roblox-VFX-Library"     # -> index.json
python scripts/find_vfx.py    --lib "~/Roblox-VFX-Library" ice ring --layered

需要综合三种信号,因为任何一种单独使用都不够:

信号覆盖率只有它能回答的问题
数值 — 从采集到的属性中提取运动和时序信息100%单次发射还是持续发射、扩散、高阻力、发光、颜色
精灵图 — 根据纹理像素识别形状97%圆环/条纹/星形/柔和——这是识别纹理色板的唯一信号
名称 — 艺术家给它起的名字29%RasenShuriken。无法从像素或数值推导得出,且优先级高于两者

综合后:98%。

为什么单靠数值不够。 纹理色板条目刻意采用中性设置—— 白色、速度为 0、扩散为 0、尺寸为 1——以便资源包展示图像。1813 个配方中, 有 1552 个在数值分类时结果完全相同。区分它们的一切信息都在像素中。

为什么精灵图分析需要翻页布局。 大多数粒子纹理是精灵图 图集,而非单张精灵图——这里 2180 个层中有 1481 个使用 Grid4x4。 测量整张图像,测到的只是平铺几何形状:无论各帧里是什么,包含圆环的 4x4 网格看起来都会密集、圆润且不空心。应根据配方声明的布局裁切单帧, 并选取墨迹最多的一帧,因为 OneShot 翻页动画的第 0 帧通常几乎是空的。

信任标签前,先审核它们。

python scripts/classify_lib.py --lib DIR --sheet ring

这会生成一张联系表,并在每个精灵图下方打印分配给它的标签。仔细查看。 这个分类器的第一个版本得分不错,却识别的是平铺几何形状;联系表让我们 发现了这一点。未经人工检查的分类器,就是一个只会说“是”的分类器。

标签会如实反映自身的置信度:is:explosion 来自名称, maybe:shockwave 是根据精灵图形状推断得出的,而 emit-inferred 表示 爆发数量是猜测值。搜索会按这个顺序为它们赋予权重。

在游戏中使用

local VFXKit = require(game.ReplicatedStorage.VFXKit)

-- "MyAura" and "MyImpact" are recipes in YOUR emitted kit (names you chose in curate.py)
local aura = VFXKit.build("MyAura", character.HumanoidRootPart, {
    tint = Color3.fromRGB(255, 120, 40),   -- keeps the luminance ramp, swaps hue
})
aura.setEnabled(true)

local impact = VFXKit.build("MyImpact", character.HumanoidRootPart, {
    offset = Vector3.new(0, -(humanoid.HipHeight + root.Size.Y * 0.5), 0),
})
impact.emit(1.3)     -- multiplier scales particle counts and light peak

opts:scale、tint、lifetime_mul、rate_mul、offset、suffix。 handle:emit(mult)、setEnabled(bool)、destroy()。

使用相同名称构建两次时,新实例会替换旧实例,而不会重复创建。若要创建有意独立的第二个实例,请使用 suffix。

Tint 会保留亮度渐变,只替换色相。 直接替换纯色会破坏从明亮核心到暗淡尾部的渐变弧线,而这正是火焰视觉效果的关键。


配方格式

{
  "name": "MyImpact",
  "duration": 1.9,              // longest tail, for previews
  "texture_portable": true,     // decided at ingest, verified against refs
  "texture_refs": ["rbxasset://textures/particles/fire_main.dds"],
  "layers": [{
    "name": "Fire",
    "class": "ParticleEmitter", // or Beam, Trail, PointLight
    "offset": [0, 0, 0],        // attachment offset from the container origin
    "emit": {"mode": "burst", "count": 16},
    "props": { /* all 31, typed */ }
  }]
}

采集的图层会带有三个额外字段,用于记录值的来源。这些字段是来源信息,而非设置;手动编写的配方中应省略它们:

"parent_class": "Model",      // what the emitter hung off in the source pack
"renders_in_place": false,    // ...and whether it could draw there at all
"emit": {
  "mode": "burst", "count": 100,
  "inferred": true,           // NOT recorded by the pack. A guess. Look at it.
  "burst_window": 0.1,        // the guess that produced count
  "harvested_rate": 1000      // the original Rate, which burst mode zeroes
}

类型化值与 Rojo .model.json 使用相同的约定,因此无需从裸数组中猜测类型:{"NumberSequence": [[t, value, envelope], ...]}、 {"ColorSequence": [[t, [r, g, b]], ...]}、{"NumberRange": [min, max]}、 {"Vector3": [x, y, z]}、{"Enum": "NormalId.Top"}。

emit.mode 可设为 burst(一次性 :Emit(count))、continuous (以 Rate 持续发射)、pulse(从 from 开始、在 seconds 内衰减的光)、constant(持续开启的光)。

Beam 和 Trail 的 Attachment0/Attachment1 是实例引用,而非值。 它们会记录为 {"Ref": "<full name>"},属于需要重建的结构,不能伪装成数据。


性能

  • Rate 表示每秒发射次数,持续发射器每帧都会产生开销。事件驱动的效果优先使用一次性 :Emit(n)。
  • 对所有需要发光的对象设置 LightInfluence = 0,否则世界光照会使其颜色偏灰。
  • LightEmission 提供免费的亮度效果,而真正的 PointLight 并非如此。典型的冲击效果配方只使用一个,并以脉冲方式持续约 0.3s。
  • 生命周期长、体积大的柔和粒子开销最高:过度绘制严重。因此,光环的生命周期应控制在约一秒以内。

评论

支持 Markdown