关于 roblox-vfx
roblox-vfx 是用于设计 Roblox 视觉效果的技能,可在浏览器模拟器中逐帧审查并导出 Studio 套件。它还提供独立流程,可从自有资源包提取发射器配置并生成 VFXKit 模块。
主要功能
- 01在模拟器中以定义形式设计效果
- 02为效果生成纹理和网格
- 03在模拟器中逐帧审查效果
- 04将效果导出为 Studio 套件
- 05从自有资源包提取发射器并生成 VFXKit
适用场景
- 01制作和调整动漫风格的 Roblox 效果
- 02检查效果在各帧的时序与外观
- 03为 Roblox Studio 准备效果
- 04复用自有免费资源包中的发射器配方
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ syphodev-yt/ roblox-ai-skills --skill roblox-vfx安装后在 Claude Code 中输入/roblox-vfx确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LOCAL.template.mdreferences/BEATS.mdCharacterVFX.server.luaDESIGN.mdHitSystem.luauproperty-surface.mdVFXKit.luareferences/corpus/CATEGORIES.mdSCHEMA.mdSTATS.jsonSTATS.mdreferences/corpus/tools/contact.pyevents.pymeasure.pypalette.pystrip.pyvtt2txt.pyreferences/styles/anime_cel.mdrealistic.mdSTYLES.mdstylised_game.mdscripts/assemble_kit.pyaudition.pyclassify_lib.pycompose.pycontact_grid.pycritic_gate.pycurate.pyemit_luau.pyemit_scenes.py详细介绍
Roblox VFX,无头模式
第一次在这台机器上运行? 请阅读此文件夹中的
LOCAL.md。如果该文件不存在,请运行roblox-skills-setup技能(或将LOCAL.template.md复制为LOCAL.md并填写)。LOCAL.md保存你的路径和工具位置;它属于你,不会被分享。
此技能有两种路线:
- 制作(常规路线,也是看起来最专业的路线)。设计特效,在浏览器模拟器中将其构建为定义,逐帧评估,然后导出 Studio 套件。所有纹理和网格均由此技能生成,因此你发布的所有内容都属于你。从 “标准” 到 “每次都制作高品质 VFX” 章节。
- 提取(用于快速原型和占位内容)。从你拥有的免费套件中读取每个发射器,将配方保存在自己的库中,然后生成一个
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 中录制和验证。评估每个新特效时,都应以此为标准,绝不低于此水平。
达到此标准的关键。以下各项都必须做到:
- 每个元素都要货真价实(见下文 RULE ZERO)。武器、生物和棺材都必须是网格,而猛击必须留下真正的弹坑和碎屑。
- 终极技要加入动漫式大场面效果: 撞击帧、FOV 冲击、聚焦线、遮幅和镜头切换(下文的模拟动作
fov、overlay、bloom)。 - 最后为每个特效进行润色:
sim/effects/fxpolish.py、polish(d)。- 能量穹顶要变成饱和度最高颜色的 ForceField 外壳,包裹着一个尺寸为 0.6x、先行消散的 Neon 核心。
- 弹坑边缘采用竞技场的石头颜色。
- 光环火焰和火花要放在 Part 上,而不是 Attachment 上。
- 彩色残影的透明度至少为 0.6。
- 持续烟雾要更小、更透。
- 使用饱和色,绝不使用苍白的混合色。 苍白的穹顶会呈现透明玻璃效果,苍白的核心会泛白。表现黑暗招式时,应让世界变得苍白,以黑色表现黑暗,并且不要在黑色上添加辉光。
- 目标要像真实身体一样做出反应(见下一节)。如果被击中的身体仍然直立漂浮,无论粒子效果多出色,呈现出来都会很不自然。
- 镜头要框住完整动作。 检查每次切镜头的第一帧和最后一帧,结尾特写保持在 4-5 studs,镜头绝不能进入施法者体内。
录制前的门槛。每项检查都必须通过,依据数值判断,不凭肉眼:
| 检查 | 通过标准 |
|---|---|
python sim/tools/sa_parent_check.py | MeshPart 之外的 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.js | three.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.py | Blender 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 网格和数学生成的纹理满足。请按顺序完成以下步骤;跳过步骤正是第一次尝试效果“不够惊艳”的原因。
- 先拆解参考作品。 下载参考作品,测量逐帧亮度和饱和度,按原始帧率剪出带编号的条带,并编写带有具体数字的阶段时间线(持续时间、增长曲线、每个阶段的颜色十六进制值,以及标明各层 Roblox 技术的图层列表)。工具:
references/corpus/tools/(events.py、strip.py、measure.py)。将较大的文件(下载内容、帧、条带)保留在LOCAL.md中指定的研究文件夹里,绝不要放进技能目录。绝不要凭空想象并创作大型效果。 - 根据测量得到的配方制作纹理。
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,程序化生成)。 - 在 Blender 中制作网格(
sim/mesh/make_meshes.py):U 轴沿运动方向并用于平铺,V 轴横跨网格,UV 范围为完整的 0..1。网格纹理(mesh_*)按 UV 空间布局,而不是作为径向精灵图。Roblox 不支持网格 UV 滚动:网格通过 Size / CFrame / Transparency / SurfaceAppearance.Color 属性补间来移动。 - 将效果构建为参数化生成器(
sim/effects/build_*.py),所有数值都来自第 1 步,并按单一尺寸参数(R)缩放。图层职责:闪光、核心、主体、次级笔触、地面、残留、屏幕色调。大型招式需要摄像机、1 帧闪光、持续的白屏效果和逐渐冷却的 ColorCorrection;普通招式不需要(实测结果:4/4 个终极技需要,0/9 个普通招式需要)。将效果添加到sim/effects/index.json,以便工作台列出它。 - 在模拟环境中评判效果,使用一条按阶段时间点编号的条带,以及
metrics()(根据亮度确定闪光帧数和峰值时间)。修正后重新生成条带。只有完成这一步后,才进行: - 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 秒天空圆形引爆效果,所有资源均为自有。
采集流程
采集器是参考和原型工具,不是素材包。 它提供工具,不包含采集的效果。你需要按以下顺序构建自己的资源库:
- 手动将免费 VFX 素材包添加到自己的 Roblox 账户。 在 Creator Store(或 Studio 的工具箱)中找到素材包,并在登录后点击其 Get / Add to Inventory 按钮。此步骤无法自动化:
insert_asset和InsertService:LoadAsset都会拒绝加载账户不拥有的资源,并显示“User is not authorized to access Asset”,即使列表显示免费也一样。免费意味着可以免费获取,不代表可以免费加载。(经测量,Roblox 自带的“Particle Effects”素材包228304820是唯一例外:无需先获取即可加载,因此可用作测试素材包。) - 将素材包放入 Studio 场景(从你的库存拖入 Workspace,可放在任意已打开的场景中)。
- 运行采集器(见下文的
payload.py harvest+recipe_server.py)。配方会保存到你自己的资源库文件夹中,该文件夹位于技能文件夹之外。路径见LOCAL.md(ROBLOX_VFX_LIB)。 - 分类、搜索、整理并导出你自己的资源库:
classify_lib.py为其添加标签,find_vfx.py搜索内容,audition.py对比候选项,curate.py复制游戏会用到的内容,emit_luau.py写入VFXKitModuleScript(使用--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.55 | 15.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/texPNG),并且重新着色时只保留灰色图层。
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会写入一个VFXScenesModuleScript(包含组合及其去重后的配方),以及一个VFXDemoLocalScript,为每个组合提供一个按键和按钮(前八个组合使用按键 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 的资源都需要先获取。 因此,真正开始收集前,坦诚地说第一步就是:将资源包添加到自己的账户中,然后再收集。
执行流程
-
启动接收服务。 这是实际的输出路径——分块打印适用于三个配方,处理两百个时就会出问题,而被截断的配方看起来和有效配方一模一样。
python scripts/recipe_server.py # lands in <ROBLOX_VFX_LIB>/harvested它会将收集结果保存到
harvested/,而不是你精心整理的recipes/文件夹,这是有意为之。若将接收服务指向recipes/,三十个未经查看的配方就会和你挑选的配方混在一起。请手动移入。 -
组装载荷。
python scripts/payload.py harvest --root Workspace.SomePack --out work/h.lua -
通过
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。如果接收服务未运行,脚本会退回到分块打印并说明原因,因此忘记启动接收服务只会变慢,不会悄无声息地截断内容。 -
验证,然后整理,再移入正式目录。
python scripts/verify_vfx.py --lib "<your library>/harvested" --quiet -
将保留的配方从
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,这就是问题特征。然后:
-
终止占用插件所用端口的服务,也就是你的工具没有连接的那个服务。终止你连接的服务会结束当前会话的 MCP。
-
从 48741 到你的端口建立原始 TCP 转发即可,因为插件会重新连接到 48741 上任何响应的服务。
-
插件仍会显示为未连接。
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 是唯一有意义的信号。
预览采集到的配方
从库中判断单个配方的流程。这里的所有内容都有实测依据; 文中提到的陷阱对应上文中的编号。
-
启动 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 -
相对于摄像机放置目标。 在编辑模式下设置
CurrentCamera.CFrame不会生效(陷阱 14)。先读取摄像机,再将部件放在cf.Position + cf.LookVector * d处。目标必须是BasePart(陷阱 3)。 在它后方放置中灰色背景,但画面中要保留一些天空或地面,否则会因纯色 故障导致捕获失败(陷阱 5)。 -
**通过
loadstring运行,**不要直接粘贴:local fn = loadstring(game:GetService("HttpService"):GetAsync("http://127.0.0.1:48752/payload/b.lua")) return fn() -
先发射,再冻结相位,最后捕获。 使用配方自带的发射数量, 不要自行编造一个数字:
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。- 生命周期长、体积大的柔和粒子开销最高:过度绘制严重。因此,光环的生命周期应控制在约一秒以内。