@adiholick/nvstudio-mcp 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/agent-plugin/rules/AGENTS.md +56 -0
- package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +217 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +14 -0
- package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
- package/dist/index.js +625 -0
- package/dist/roblox-bridge.js +108 -0
- package/dist/vfx-database.js +507 -0
- package/package.json +2 -2
- package/public/index.html +913 -14
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/animate_character.lua +36 -0
- package/studio-plugin/apply_vfx.lua +59 -0
- package/studio-plugin/edit_rig.lua +35 -0
- package/studio-plugin/nv_anim_dispatcher.lua +239 -0
- package/studio-plugin/nv_auto_rigger.lua +392 -0
- package/studio-plugin/nv_curves.lua +260 -0
- package/studio-plugin/nv_joint_math.lua +138 -0
- package/studio-plugin/nv_kinematics.lua +248 -0
- package/studio-plugin/nv_motion_generator.lua +279 -0
- package/studio-plugin/nv_rig_dispatcher.lua +319 -0
- package/studio-plugin/nv_vfx_serializer.lua +363 -0
- package/studio-plugin/nvstudio_mcp.rbxmx +3008 -115
- package/studio-plugin/scan_vfx.lua +85 -0
- package/studio-plugin/scrub_timeline.lua +122 -0
- package/studio-plugin/set_properties.lua +73 -0
- package/studio-plugin/solve_limb_ik.lua +63 -0
- package/studio-plugin/ui_manager.lua +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
3
|
+
-- NVStudio Subsystem: nv_auto_rigger
|
|
4
|
+
-- Deskripsi: Mesin Auto-Rigging Deterministik & Heuristik Geometris
|
|
5
|
+
-- Membangun struktur rig lengkap untuk Avatar R6, R15, Senjata/Prop,
|
|
6
|
+
-- dan Assembly Model secara instan dengan zero-dislocation math.
|
|
7
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
local jointMathMod = script.Parent:WaitForChild("nv_joint_math")
|
|
10
|
+
local JointMath = require(jointMathMod)
|
|
11
|
+
|
|
12
|
+
local AutoRigger = {}
|
|
13
|
+
|
|
14
|
+
-- ── Helper: Dapatkan atau buat Motor6D ─────────────────────────────────────────
|
|
15
|
+
local function createOrUpdateMotor(name: string, part0: BasePart, part1: BasePart, parent: Instance?): Motor6D
|
|
16
|
+
part1.Anchored = false
|
|
17
|
+
local existing = part0:FindFirstChild(name) or part1:FindFirstChild(name)
|
|
18
|
+
local motor: Motor6D
|
|
19
|
+
if existing and existing:IsA("Motor6D") then
|
|
20
|
+
motor = existing
|
|
21
|
+
else
|
|
22
|
+
motor = Instance.new("Motor6D")
|
|
23
|
+
motor.Name = name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
motor.Part0 = part0
|
|
27
|
+
motor.Part1 = part1
|
|
28
|
+
local c0, c1 = JointMath.computeInitialOffsets(part0, part1)
|
|
29
|
+
motor.C0 = c0
|
|
30
|
+
motor.C1 = c1
|
|
31
|
+
motor.Parent = parent or part0
|
|
32
|
+
return motor
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
-- ── 1. Auto-Rig R6 Avatar ─────────────────────────────────────────────────────
|
|
36
|
+
function AutoRigger.rigR6(model: Model): { [string]: any }
|
|
37
|
+
assert(model and model:IsA("Model"), "Target harus berupa Model")
|
|
38
|
+
|
|
39
|
+
local parts: { [string]: BasePart } = {}
|
|
40
|
+
for _, desc in ipairs(model:GetDescendants()) do
|
|
41
|
+
if desc:IsA("BasePart") then
|
|
42
|
+
parts[desc.Name] = desc
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
local torso = parts["Torso"] or parts["UpperTorso"]
|
|
47
|
+
if not torso then
|
|
48
|
+
return { status = "error", error = "Model tidak memiliki part 'Torso' untuk R6 rig." }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
local rootPart = parts["HumanoidRootPart"]
|
|
52
|
+
if not rootPart then
|
|
53
|
+
-- Buat HumanoidRootPart transparan jika belum ada
|
|
54
|
+
rootPart = Instance.new("Part")
|
|
55
|
+
rootPart.Name = "HumanoidRootPart"
|
|
56
|
+
rootPart.Size = Vector3.new(2, 2, 1)
|
|
57
|
+
rootPart.Transparency = 1
|
|
58
|
+
rootPart.CanCollide = false
|
|
59
|
+
rootPart.CFrame = torso.CFrame
|
|
60
|
+
rootPart.Parent = model
|
|
61
|
+
parts["HumanoidRootPart"] = rootPart
|
|
62
|
+
model.PrimaryPart = rootPart
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local created = {}
|
|
66
|
+
|
|
67
|
+
-- 1. RootJoint: HumanoidRootPart -> Torso
|
|
68
|
+
local rootJoint = createOrUpdateMotor("RootJoint", rootPart, torso, rootPart)
|
|
69
|
+
table.insert(created, "RootJoint")
|
|
70
|
+
|
|
71
|
+
-- 2. Neck: Torso -> Head
|
|
72
|
+
if parts["Head"] then
|
|
73
|
+
createOrUpdateMotor("Neck", torso, parts["Head"], torso)
|
|
74
|
+
table.insert(created, "Neck")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
-- 3. Shoulders: Torso -> Arms
|
|
78
|
+
local lArm = parts["Left Arm"] or parts["LeftArm"]
|
|
79
|
+
if lArm then
|
|
80
|
+
createOrUpdateMotor("Left Shoulder", torso, lArm, torso)
|
|
81
|
+
table.insert(created, "Left Shoulder")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
local rArm = parts["Right Arm"] or parts["RightArm"]
|
|
85
|
+
if rArm then
|
|
86
|
+
createOrUpdateMotor("Right Shoulder", torso, rArm, torso)
|
|
87
|
+
table.insert(created, "Right Shoulder")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
-- 4. Hips: Torso -> Legs
|
|
91
|
+
local lLeg = parts["Left Leg"] or parts["LeftLeg"]
|
|
92
|
+
if lLeg then
|
|
93
|
+
createOrUpdateMotor("Left Hip", torso, lLeg, torso)
|
|
94
|
+
table.insert(created, "Left Hip")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
local rLeg = parts["Right Leg"] or parts["RightLeg"]
|
|
98
|
+
if rLeg then
|
|
99
|
+
createOrUpdateMotor("Right Hip", torso, rLeg, torso)
|
|
100
|
+
table.insert(created, "Right Hip")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
-- Pastikan Humanoid bertipe R6
|
|
104
|
+
local humanoid = model:FindFirstChildOfClass("Humanoid")
|
|
105
|
+
if humanoid then
|
|
106
|
+
humanoid.RigType = Enum.HumanoidRigType.R6
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
status = "success",
|
|
111
|
+
rigType = "R6",
|
|
112
|
+
jointsCreated = #created,
|
|
113
|
+
jointNames = created,
|
|
114
|
+
rootPart = rootPart.Name
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
-- ── 2. Auto-Rig R15 Avatar ────────────────────────────────────────────────────
|
|
119
|
+
function AutoRigger.rigR15(model: Model): { [string]: any }
|
|
120
|
+
assert(model and model:IsA("Model"), "Target harus berupa Model")
|
|
121
|
+
|
|
122
|
+
local parts: { [string]: BasePart } = {}
|
|
123
|
+
for _, desc in ipairs(model:GetDescendants()) do
|
|
124
|
+
if desc:IsA("BasePart") then
|
|
125
|
+
parts[desc.Name] = desc
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
local lowerTorso = parts["LowerTorso"]
|
|
130
|
+
local upperTorso = parts["UpperTorso"]
|
|
131
|
+
if not lowerTorso or not upperTorso then
|
|
132
|
+
return { status = "error", error = "Model tidak memiliki 'LowerTorso' dan 'UpperTorso' untuk R15 rig." }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
local rootPart = parts["HumanoidRootPart"]
|
|
136
|
+
if not rootPart then
|
|
137
|
+
rootPart = Instance.new("Part")
|
|
138
|
+
rootPart.Name = "HumanoidRootPart"
|
|
139
|
+
rootPart.Size = Vector3.new(2, 2, 1)
|
|
140
|
+
rootPart.Transparency = 1
|
|
141
|
+
rootPart.CanCollide = false
|
|
142
|
+
rootPart.CFrame = lowerTorso.CFrame
|
|
143
|
+
rootPart.Parent = model
|
|
144
|
+
parts["HumanoidRootPart"] = rootPart
|
|
145
|
+
model.PrimaryPart = rootPart
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
local r15Hierarchy = {
|
|
149
|
+
{ name = "Root", p0 = "HumanoidRootPart", p1 = "LowerTorso" },
|
|
150
|
+
{ name = "Waist", p0 = "LowerTorso", p1 = "UpperTorso" },
|
|
151
|
+
{ name = "Neck", p0 = "UpperTorso", p1 = "Head" },
|
|
152
|
+
|
|
153
|
+
-- Left Arm Chain
|
|
154
|
+
{ name = "LeftShoulder", p0 = "UpperTorso", p1 = "LeftUpperArm" },
|
|
155
|
+
{ name = "LeftElbow", p0 = "LeftUpperArm", p1 = "LeftLowerArm" },
|
|
156
|
+
{ name = "LeftWrist", p0 = "LeftLowerArm", p1 = "LeftHand" },
|
|
157
|
+
|
|
158
|
+
-- Right Arm Chain
|
|
159
|
+
{ name = "RightShoulder", p0 = "UpperTorso", p1 = "RightUpperArm" },
|
|
160
|
+
{ name = "RightElbow", p0 = "RightUpperArm", p1 = "RightLowerArm" },
|
|
161
|
+
{ name = "RightWrist", p0 = "RightLowerArm", p1 = "RightHand" },
|
|
162
|
+
|
|
163
|
+
-- Left Leg Chain
|
|
164
|
+
{ name = "LeftHip", p0 = "LowerTorso", p1 = "LeftUpperLeg" },
|
|
165
|
+
{ name = "LeftKnee", p0 = "LeftUpperLeg", p1 = "LeftLowerLeg" },
|
|
166
|
+
{ name = "LeftAnkle", p0 = "LeftLowerLeg", p1 = "LeftFoot" },
|
|
167
|
+
|
|
168
|
+
-- Right Leg Chain
|
|
169
|
+
{ name = "RightHip", p0 = "LowerTorso", p1 = "RightUpperLeg" },
|
|
170
|
+
{ name = "RightKnee", p0 = "RightUpperLeg", p1 = "RightLowerLeg" },
|
|
171
|
+
{ name = "RightAnkle", p0 = "RightLowerLeg", p1 = "RightFoot" }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
local created = {}
|
|
175
|
+
for _, item in ipairs(r15Hierarchy) do
|
|
176
|
+
local p0 = parts[item.p0]
|
|
177
|
+
local p1 = parts[item.p1]
|
|
178
|
+
if p0 and p1 then
|
|
179
|
+
createOrUpdateMotor(item.name, p0, p1, p0)
|
|
180
|
+
table.insert(created, item.name)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local humanoid = model:FindFirstChildOfClass("Humanoid")
|
|
185
|
+
if humanoid then
|
|
186
|
+
humanoid.RigType = Enum.HumanoidRigType.R15
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
status = "success",
|
|
191
|
+
rigType = "R15",
|
|
192
|
+
jointsCreated = #created,
|
|
193
|
+
jointNames = created,
|
|
194
|
+
rootPart = rootPart.Name
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
-- ── 3. Rig Weapon / Tool ke Karakter ──────────────────────────────────────────
|
|
199
|
+
function AutoRigger.rigWeapon(characterModel: Model, weaponInstance: Instance, targetLimbName: string?): { [string]: any }
|
|
200
|
+
assert(characterModel and characterModel:IsA("Model"), "Character target harus berupa Model")
|
|
201
|
+
assert(weaponInstance and weaponInstance:IsA("Instance"), "Weapon target tidak valid")
|
|
202
|
+
|
|
203
|
+
local parts: { [string]: BasePart } = {}
|
|
204
|
+
for _, desc in ipairs(characterModel:GetDescendants()) do
|
|
205
|
+
if desc:IsA("BasePart") then
|
|
206
|
+
parts[desc.Name] = desc
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
local limb = targetLimbName and parts[targetLimbName]
|
|
211
|
+
if not limb then
|
|
212
|
+
limb = parts["RightHand"] or parts["Right Arm"] or parts["RightArm"] or parts["HumanoidRootPart"]
|
|
213
|
+
end
|
|
214
|
+
if not limb then
|
|
215
|
+
return { status = "error", error = "Tidak dapat menemukan anggota tubuh target (RightHand / Right Arm) pada karakter." }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
-- Cari handle pada senjata
|
|
219
|
+
local weaponHandle: BasePart? = nil
|
|
220
|
+
if weaponInstance:IsA("BasePart") then
|
|
221
|
+
weaponHandle = weaponInstance
|
|
222
|
+
elseif weaponInstance:IsA("Model") or weaponInstance:IsA("Tool") then
|
|
223
|
+
weaponHandle = (weaponInstance :: any).PrimaryPart
|
|
224
|
+
or weaponInstance:FindFirstChild("Handle") :: BasePart?
|
|
225
|
+
or weaponInstance:FindFirstChildWhichIsA("BasePart")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if not weaponHandle then
|
|
229
|
+
return { status = "error", error = "Senjata tidak memiliki BasePart atau Handle." }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
-- 1. Buat Motor6D penaut karakter ke Handle
|
|
233
|
+
local weaponMotor = createOrUpdateMotor("WeaponGrip", limb, weaponHandle, limb)
|
|
234
|
+
|
|
235
|
+
-- 2. Las seluruh bagian senjata lain ke Handle
|
|
236
|
+
local weldedCount = 0
|
|
237
|
+
if weaponInstance ~= weaponHandle then
|
|
238
|
+
for _, desc in ipairs(weaponInstance:GetDescendants()) do
|
|
239
|
+
if desc:IsA("BasePart") and desc ~= weaponHandle then
|
|
240
|
+
desc.Anchored = false
|
|
241
|
+
local weld = desc:FindFirstChild("WeldToHandle") or Instance.new("Weld")
|
|
242
|
+
weld.Name = "WeldToHandle"
|
|
243
|
+
weld.Part0 = weaponHandle
|
|
244
|
+
weld.Part1 = desc
|
|
245
|
+
weld.C0 = weaponHandle.CFrame:ToObjectSpace(desc.CFrame)
|
|
246
|
+
weld.C1 = CFrame.identity
|
|
247
|
+
weld.Parent = weaponHandle
|
|
248
|
+
weldedCount += 1
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
status = "success",
|
|
255
|
+
attachedTo = limb.Name,
|
|
256
|
+
handleName = weaponHandle.Name,
|
|
257
|
+
weldedComponents = weldedCount,
|
|
258
|
+
message = string.format("Senjata '%s' berhasil dirig ke '%s' (%d part dilas)", weaponInstance.Name, limb.Name, weldedCount)
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
-- ── 4. Weld Assembly (Props, Armor, Kendaraan) ────────────────────────────────
|
|
263
|
+
function AutoRigger.weldAssembly(container: Instance, primaryPart: BasePart?): { [string]: any }
|
|
264
|
+
assert(container and container:IsA("Instance"), "Container tidak valid")
|
|
265
|
+
|
|
266
|
+
local rootPart: BasePart? = primaryPart
|
|
267
|
+
if not rootPart and container:IsA("Model") then
|
|
268
|
+
rootPart = container.PrimaryPart
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
local allParts: { BasePart } = {}
|
|
272
|
+
for _, desc in ipairs(container:GetDescendants()) do
|
|
273
|
+
if desc:IsA("BasePart") then
|
|
274
|
+
table.insert(allParts, desc)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
if container:IsA("BasePart") then
|
|
278
|
+
table.insert(allParts, container)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if #allParts == 0 then
|
|
282
|
+
return { status = "error", error = "Tidak ada BasePart di dalam kontainer untuk dilas." }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
-- Jika belum ada rootPart, tentukan berdasarkan volume terbesar
|
|
286
|
+
if not rootPart then
|
|
287
|
+
local maxVol = -1
|
|
288
|
+
for _, p in ipairs(allParts) do
|
|
289
|
+
local vol = p.Size.X * p.Size.Y * p.Size.Z
|
|
290
|
+
if vol > maxVol then
|
|
291
|
+
maxVol = vol
|
|
292
|
+
rootPart = p
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if not rootPart then
|
|
298
|
+
return { status = "error", error = "Gagal menentukan Root Part untuk pengelasan." }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
local weldsCreated = 0
|
|
302
|
+
for _, p in ipairs(allParts) do
|
|
303
|
+
if p ~= rootPart then
|
|
304
|
+
p.Anchored = false
|
|
305
|
+
local w = p:FindFirstChild("AssemblyWeld") or Instance.new("Weld")
|
|
306
|
+
w.Name = "AssemblyWeld"
|
|
307
|
+
w.Part0 = rootPart
|
|
308
|
+
w.Part1 = p
|
|
309
|
+
w.C0 = rootPart.CFrame:ToObjectSpace(p.CFrame)
|
|
310
|
+
w.C1 = CFrame.identity
|
|
311
|
+
w.Parent = rootPart
|
|
312
|
+
weldsCreated += 1
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
status = "success",
|
|
318
|
+
rootPart = rootPart.Name,
|
|
319
|
+
weldsCreated = weldsCreated,
|
|
320
|
+
message = string.format("Berhasil mengelas %d part ke Root '%s'", weldsCreated, rootPart.Name)
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
-- ── 5. Spatial Geometric Limb Analyzer (Heuristik Bounding Box) ───────────────
|
|
325
|
+
-- Menganalisis model dengan nama part acak/impor dan mengklasifikasikan anggota tubuh
|
|
326
|
+
function AutoRigger.discoverLimbsBySpatialGeometry(model: Model): { [string]: BasePart }
|
|
327
|
+
local parts: { BasePart } = {}
|
|
328
|
+
for _, desc in ipairs(model:GetDescendants()) do
|
|
329
|
+
if desc:IsA("BasePart") then
|
|
330
|
+
table.insert(parts, desc)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if #parts == 0 then return {} end
|
|
335
|
+
|
|
336
|
+
-- 1. Hitung bounding box pusat model
|
|
337
|
+
local cframe, size = model:GetBoundingBox()
|
|
338
|
+
local center = cframe.Position
|
|
339
|
+
|
|
340
|
+
-- 2. Temukan torso (part dengan volume besar di sekitar pusat vertikal)
|
|
341
|
+
local bestTorso: BasePart = parts[1]
|
|
342
|
+
local bestScore = -1e9
|
|
343
|
+
|
|
344
|
+
for _, p in ipairs(parts) do
|
|
345
|
+
local vol = p.Size.X * p.Size.Y * p.Size.Z
|
|
346
|
+
local distToCenter = (p.Position - center).Magnitude
|
|
347
|
+
local score = vol / (distToCenter + 1)
|
|
348
|
+
if score > bestScore then
|
|
349
|
+
bestScore = score
|
|
350
|
+
bestTorso = p
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
local identified: { [string]: BasePart } = {
|
|
355
|
+
Torso = bestTorso
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
local torsoPos = bestTorso.Position
|
|
359
|
+
|
|
360
|
+
-- 3. Klasifikasi part lainnya relatif terhadap Torso
|
|
361
|
+
for _, p in ipairs(parts) do
|
|
362
|
+
if p ~= bestTorso then
|
|
363
|
+
local relPos = bestTorso.CFrame:PointToObjectSpace(p.Position)
|
|
364
|
+
|
|
365
|
+
if relPos.Y > bestTorso.Size.Y * 0.5 then
|
|
366
|
+
-- Bagian atas -> Head
|
|
367
|
+
if not identified["Head"] or p.Position.Y > identified["Head"].Position.Y then
|
|
368
|
+
identified["Head"] = p
|
|
369
|
+
end
|
|
370
|
+
elseif relPos.Y < -bestTorso.Size.Y * 0.3 then
|
|
371
|
+
-- Bagian bawah -> Legs
|
|
372
|
+
if relPos.X < 0 then
|
|
373
|
+
identified["Left Leg"] = p
|
|
374
|
+
else
|
|
375
|
+
identified["Right Leg"] = p
|
|
376
|
+
end
|
|
377
|
+
else
|
|
378
|
+
-- Bagian samping -> Arms
|
|
379
|
+
if relPos.X < 0 then
|
|
380
|
+
identified["Left Arm"] = p
|
|
381
|
+
else
|
|
382
|
+
identified["Right Arm"] = p
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
return identified
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
return AutoRigger
|
|
392
|
+
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!native
|
|
3
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
4
|
+
-- NVStudio Subsystem: nv_curves
|
|
5
|
+
-- Deskripsi: Mesin Kurva Cubic Bezier, Easing Functions & Quaternion SLERP
|
|
6
|
+
-- Menghitung interpolasi waktu dan rotasi sendi secara ultra-halus
|
|
7
|
+
-- layaknya kurva Graph Editor animator profesional.
|
|
8
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
local Curves = {}
|
|
11
|
+
local PI = math.pi
|
|
12
|
+
local HALF_PI = math.pi * 0.5
|
|
13
|
+
local TAU = math.pi * 2
|
|
14
|
+
local DEFAULT_OVERSHOOT = 1.70158
|
|
15
|
+
|
|
16
|
+
-- ── 1. Cubic Bezier Solver ───────────────────────────────────────────────────
|
|
17
|
+
function Curves.bezierParameter(alpha: number, x1: number, x2: number): number
|
|
18
|
+
if alpha <= 0 then return 0 end
|
|
19
|
+
if alpha >= 1 then return 1 end
|
|
20
|
+
|
|
21
|
+
x1 = math.clamp(x1, 0, 1)
|
|
22
|
+
x2 = math.clamp(x2, 0, 1)
|
|
23
|
+
|
|
24
|
+
local lo, hi, u = 0, 1, alpha
|
|
25
|
+
for _ = 1, 32 do
|
|
26
|
+
local v = 1 - u
|
|
27
|
+
local x = 3 * v * v * u * x1 + 3 * v * u * u * x2 + u * u * u
|
|
28
|
+
local delta = x - alpha
|
|
29
|
+
if math.abs(delta) <= 1e-7 then
|
|
30
|
+
break
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if delta < 0 then
|
|
34
|
+
lo = u
|
|
35
|
+
else
|
|
36
|
+
hi = u
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local deriv = 3 * v * v * x1 + 6 * v * u * (x2 - x1) + 3 * u * u * (1 - x2)
|
|
40
|
+
local cand = deriv > 1e-7 and (u - delta / deriv) or -1
|
|
41
|
+
u = (cand > lo and cand < hi) and cand or (lo + hi) * 0.5
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return u
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
function Curves.cubic(a: number, b: number, c: number, d: number, u: number): number
|
|
48
|
+
local v = 1 - u
|
|
49
|
+
return a * (v * v * v) + b * (3 * v * v * u) + c * (3 * v * u * u) + d * (u * u * u)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
function Curves.bezierAlpha(t: number, x1: number?, y1: number?, x2: number?, y2: number?): number
|
|
53
|
+
if t <= 0 then return 0 end
|
|
54
|
+
if t >= 1 then return 1 end
|
|
55
|
+
|
|
56
|
+
local px1 = x1 or 0.42
|
|
57
|
+
local py1 = y1 or 0.0
|
|
58
|
+
local px2 = x2 or 0.58
|
|
59
|
+
local py2 = y2 or 1.0
|
|
60
|
+
|
|
61
|
+
local u = Curves.bezierParameter(t, px1, px2)
|
|
62
|
+
return Curves.cubic(0, py1, py2, 1, u)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
-- ── 2. Standard Easing Functions ─────────────────────────────────────────────
|
|
66
|
+
local EasingTable: { [string]: (t: number) -> number } = {
|
|
67
|
+
Linear = function(t) return t end,
|
|
68
|
+
|
|
69
|
+
SineIn = function(t) return 1 - math.cos(t * HALF_PI) end,
|
|
70
|
+
SineOut = function(t) return math.sin(t * HALF_PI) end,
|
|
71
|
+
SineInOut = function(t) return -(math.cos(PI * t) - 1) * 0.5 end,
|
|
72
|
+
|
|
73
|
+
QuadIn = function(t) return t * t end,
|
|
74
|
+
QuadOut = function(t) return t * (2 - t) end,
|
|
75
|
+
QuadInOut = function(t) return t < 0.5 and (2 * t * t) or (-1 + (4 - 2 * t) * t) end,
|
|
76
|
+
|
|
77
|
+
CubicIn = function(t) return t * t * t end,
|
|
78
|
+
CubicOut = function(t) local f = t - 1 return f * f * f + 1 end,
|
|
79
|
+
CubicInOut = function(t) return t < 0.5 and (4 * t * t * t) or ((t - 1) * (2 * t - 2) * (2 * t - 2) + 1) end,
|
|
80
|
+
|
|
81
|
+
QuartIn = function(t) return t * t * t * t end,
|
|
82
|
+
QuartOut = function(t) local f = t - 1 return 1 - f * f * f * f end,
|
|
83
|
+
QuartInOut = function(t) local f = t - 1 return t < 0.5 and (8 * t * t * t * t) or (1 - 8 * f * f * f * f) end,
|
|
84
|
+
|
|
85
|
+
ExpoIn = function(t) return t == 0 and 0 or math.pow(2, 10 * (t - 1)) end,
|
|
86
|
+
ExpoOut = function(t) return t == 1 and 1 or (1 - math.pow(2, -10 * t)) end,
|
|
87
|
+
ExpoInOut = function(t)
|
|
88
|
+
if t == 0 then return 0 end
|
|
89
|
+
if t == 1 then return 1 end
|
|
90
|
+
if t < 0.5 then return math.pow(2, 20 * t - 10) * 0.5 end
|
|
91
|
+
return (2 - math.pow(2, -20 * t + 10)) * 0.5
|
|
92
|
+
end,
|
|
93
|
+
|
|
94
|
+
BackIn = function(t) local s = DEFAULT_OVERSHOOT return t * t * ((s + 1) * t - s) end,
|
|
95
|
+
BackOut = function(t) local s = DEFAULT_OVERSHOOT local f = t - 1 return f * f * ((s + 1) * f + s) + 1 end,
|
|
96
|
+
BackInOut = function(t)
|
|
97
|
+
local s = DEFAULT_OVERSHOOT * 1.525
|
|
98
|
+
local f = t * 2
|
|
99
|
+
if f < 1 then return 0.5 * (f * f * ((s + 1) * f - s)) end
|
|
100
|
+
f = f - 2
|
|
101
|
+
return 0.5 * (f * f * ((s + 1) * f + s) + 2)
|
|
102
|
+
end,
|
|
103
|
+
|
|
104
|
+
BounceOut = function(t)
|
|
105
|
+
if t < (1 / 2.75) then
|
|
106
|
+
return 7.5625 * t * t
|
|
107
|
+
elseif t < (2 / 2.75) then
|
|
108
|
+
local f = t - (1.5 / 2.75)
|
|
109
|
+
return 7.5625 * f * f + 0.75
|
|
110
|
+
elseif t < (2.5 / 2.75) then
|
|
111
|
+
local f = t - (2.25 / 2.75)
|
|
112
|
+
return 7.5625 * f * f + 0.9375
|
|
113
|
+
else
|
|
114
|
+
local f = t - (2.625 / 2.75)
|
|
115
|
+
return 7.5625 * f * f + 0.984375
|
|
116
|
+
end
|
|
117
|
+
end,
|
|
118
|
+
BounceIn = function(t)
|
|
119
|
+
local fn = nil
|
|
120
|
+
-- forward declaration
|
|
121
|
+
return 1 - (function(x)
|
|
122
|
+
if x < (1 / 2.75) then return 7.5625 * x * x
|
|
123
|
+
elseif x < (2 / 2.75) then local f = x - (1.5 / 2.75) return 7.5625 * f * f + 0.75
|
|
124
|
+
elseif x < (2.5 / 2.75) then local f = x - (2.25 / 2.75) return 7.5625 * f * f + 0.9375
|
|
125
|
+
else local f = x - (2.625 / 2.75) return 7.5625 * f * f + 0.984375 end
|
|
126
|
+
end)(1 - t)
|
|
127
|
+
end,
|
|
128
|
+
BounceInOut = function(t)
|
|
129
|
+
local bounceOut = function(x: number): number
|
|
130
|
+
if x < (1 / 2.75) then return 7.5625 * x * x
|
|
131
|
+
elseif x < (2 / 2.75) then local f = x - (1.5 / 2.75) return 7.5625 * f * f + 0.75
|
|
132
|
+
elseif x < (2.5 / 2.75) then local f = x - (2.25 / 2.75) return 7.5625 * f * f + 0.9375
|
|
133
|
+
else local f = x - (2.625 / 2.75) return 7.5625 * f * f + 0.984375 end
|
|
134
|
+
end
|
|
135
|
+
if t < 0.5 then
|
|
136
|
+
return (1 - bounceOut(1 - t * 2)) * 0.5
|
|
137
|
+
end
|
|
138
|
+
return bounceOut(t * 2 - 1) * 0.5 + 0.5
|
|
139
|
+
end,
|
|
140
|
+
|
|
141
|
+
ElasticOut = function(t)
|
|
142
|
+
if t == 0 then return 0 end
|
|
143
|
+
if t == 1 then return 1 end
|
|
144
|
+
local p = 0.3
|
|
145
|
+
local s = p / 4
|
|
146
|
+
return math.pow(2, -10 * t) * math.sin((t - s) * TAU / p) + 1
|
|
147
|
+
end,
|
|
148
|
+
ElasticIn = function(t)
|
|
149
|
+
if t == 0 then return 0 end
|
|
150
|
+
if t == 1 then return 1 end
|
|
151
|
+
local p = 0.3
|
|
152
|
+
local s = p / 4
|
|
153
|
+
local f = t - 1
|
|
154
|
+
return -(math.pow(2, 10 * f) * math.sin((f - s) * TAU / p))
|
|
155
|
+
end,
|
|
156
|
+
ElasticInOut = function(t)
|
|
157
|
+
if t == 0 then return 0 end
|
|
158
|
+
if t == 1 then return 1 end
|
|
159
|
+
local p = 0.3 * 1.5
|
|
160
|
+
local s = p / 4
|
|
161
|
+
local f = t * 2 - 1
|
|
162
|
+
if f < 0 then
|
|
163
|
+
return -0.5 * (math.pow(2, 10 * f) * math.sin((f - s) * TAU / p))
|
|
164
|
+
end
|
|
165
|
+
return math.pow(2, -10 * f) * math.sin((f - s) * TAU / p) * 0.5 + 1
|
|
166
|
+
end
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function Curves.evaluate(easingName: string?, t: number): number
|
|
170
|
+
t = math.clamp(t, 0, 1)
|
|
171
|
+
local name = easingName or "QuadInOut"
|
|
172
|
+
local fn = EasingTable[name]
|
|
173
|
+
if fn then
|
|
174
|
+
return fn(t)
|
|
175
|
+
end
|
|
176
|
+
return t
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
-- ── 3. Quaternion SLERP (Spherical Linear Interpolation) ──────────────────────
|
|
180
|
+
-- Representasi Quaternion: { x, y, z, w }
|
|
181
|
+
type Quat = { x: number, y: number, z: number, w: number }
|
|
182
|
+
|
|
183
|
+
local function cframeToQuat(cf: CFrame): (Vector3, Quat)
|
|
184
|
+
local _, _, _, m00, m01, m02, m10, m11, m12, m20, m21, m22 = cf:GetComponents()
|
|
185
|
+
local trace = m00 + m11 + m22
|
|
186
|
+
local q: Quat
|
|
187
|
+
if trace > 0 then
|
|
188
|
+
local s = math.sqrt(trace + 1) * 2
|
|
189
|
+
q = { x = (m21 - m12) / s, y = (m02 - m20) / s, z = (m10 - m01) / s, w = 0.25 * s }
|
|
190
|
+
elseif (m00 > m11) and (m00 > m22) then
|
|
191
|
+
local s = math.sqrt(1 + m00 - m11 - m22) * 2
|
|
192
|
+
q = { x = 0.25 * s, y = (m01 + m10) / s, z = (m02 + m20) / s, w = (m21 - m12) / s }
|
|
193
|
+
elseif m11 > m22 then
|
|
194
|
+
local s = math.sqrt(1 + m11 - m00 - m22) * 2
|
|
195
|
+
q = { x = (m01 + m10) / s, y = 0.25 * s, z = (m12 + m21) / s, w = (m02 - m20) / s }
|
|
196
|
+
else
|
|
197
|
+
local s = math.sqrt(1 + m22 - m00 - m11) * 2
|
|
198
|
+
q = { x = (m02 + m20) / s, y = (m12 + m21) / s, z = 0.25 * s, w = (m10 - m01) / s }
|
|
199
|
+
end
|
|
200
|
+
return cf.Position, q
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
local function quatToCFrame(pos: Vector3, q: Quat): CFrame
|
|
204
|
+
local x, y, z, w = q.x, q.y, q.z, q.w
|
|
205
|
+
local xx, yy, zz = x * x, y * y, z * z
|
|
206
|
+
local xy, xz, yz = x * y, x * z, y * z
|
|
207
|
+
local wx, wy, wz = w * x, w * y, w * z
|
|
208
|
+
|
|
209
|
+
return CFrame.new(
|
|
210
|
+
pos.X, pos.Y, pos.Z,
|
|
211
|
+
1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy),
|
|
212
|
+
2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx),
|
|
213
|
+
2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
function Curves.slerp(cfA: CFrame, cfB: CFrame, alpha: number): CFrame
|
|
218
|
+
if alpha <= 0 then return cfA end
|
|
219
|
+
if alpha >= 1 then return cfB end
|
|
220
|
+
|
|
221
|
+
local posA, qA = cframeToQuat(cfA)
|
|
222
|
+
local posB, qB = cframeToQuat(cfB)
|
|
223
|
+
|
|
224
|
+
local cosHalfTheta = qA.x * qB.x + qA.y * qB.y + qA.z * qB.z + qA.w * qB.w
|
|
225
|
+
if cosHalfTheta < 0 then
|
|
226
|
+
qB = { x = -qB.x, y = -qB.y, z = -qB.z, w = -qB.w }
|
|
227
|
+
cosHalfTheta = -cosHalfTheta
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
local qResult: Quat
|
|
231
|
+
if math.abs(cosHalfTheta) >= 1 then
|
|
232
|
+
qResult = qA
|
|
233
|
+
else
|
|
234
|
+
local halfTheta = math.acos(math.clamp(cosHalfTheta, -1, 1))
|
|
235
|
+
local sinHalfTheta = math.sqrt(1 - cosHalfTheta * cosHalfTheta)
|
|
236
|
+
if math.abs(sinHalfTheta) < 1e-4 then
|
|
237
|
+
qResult = {
|
|
238
|
+
x = qA.x * (1 - alpha) + qB.x * alpha,
|
|
239
|
+
y = qA.y * (1 - alpha) + qB.y * alpha,
|
|
240
|
+
z = qA.z * (1 - alpha) + qB.z * alpha,
|
|
241
|
+
w = qA.w * (1 - alpha) + qB.w * alpha,
|
|
242
|
+
}
|
|
243
|
+
else
|
|
244
|
+
local ratioA = math.sin((1 - alpha) * halfTheta) / sinHalfTheta
|
|
245
|
+
local ratioB = math.sin(alpha * halfTheta) / sinHalfTheta
|
|
246
|
+
qResult = {
|
|
247
|
+
x = qA.x * ratioA + qB.x * ratioB,
|
|
248
|
+
y = qA.y * ratioA + qB.y * ratioB,
|
|
249
|
+
z = qA.z * ratioA + qB.z * ratioB,
|
|
250
|
+
w = qA.w * ratioA + qB.w * ratioB,
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
local interpPos = posA:Lerp(posB, alpha)
|
|
256
|
+
return quatToCFrame(interpPos, qResult)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
return Curves
|
|
260
|
+
|