SyntaxGameServer/RCCService2021/internalscripts/thumbnails/AnimationManifest.lua

376 lines
11 KiB
Lua

-- AnimationManifest.txt
-- V 1.0.3
-- Exports multiple frames
-- https://spocktopus.roblox.local/Internals_of_3D_Animated_Thumbnails
local baseUrl, characterAppearanceUrl, animationUrl = ...
local ThumbnailGenerator = game:GetService("ThumbnailGenerator")
ThumbnailGenerator:AddProfilingCheckpoint("ThumbnailScriptStarted")
local BundleLoader = require(ThumbnailGenerator:GetThumbnailModule("BundleLoader"))
pcall(function() game:GetService("ContentProvider"):SetBaseUrl(baseUrl) end)
game:GetService("ScriptContext").ScriptsDisabled = true
local FRAME_RATE = 60
local originalJointCFramesMap = {}
local originalPartsCFramesMap = {}
local function getOriginalJointCFrames(object)
for _, child in pairs(object:GetChildren()) do
if child:IsA("Motor6D") then
originalJointCFramesMap[child] = child.C1
end
if child:IsA("BasePart") then
originalPartsCFramesMap[child] = child.CFrame
end
getOriginalJointCFrames(child)
end
end
local animationObjects = game:GetObjects(animationUrl)
local animations = {}
local rotateCharacter = false
local function getAnimations(model)
for _, child in pairs(model:GetChildren()) do
if child:IsA("StringValue") and child.Name == "swim" then
rotateCharacter = true
elseif child:IsA("StringValue") and child.Name == "swimidle" then
-- Don't include swimidle if we are thumnailing the swim animation
if child.Parent and child.Parent:FindFirstChild("swim") then
return
end
end
if child:IsA("Animation") then
table.insert(animations, child)
else
getAnimations(child)
end
end
end
for _, animationModel in pairs(animationObjects) do
if animationModel:IsA("Animation") then
table.insert(animations, animationModel)
else
getAnimations(animationModel)
end
end
local bundleId
if #animations == 1 then
local anim = animations[1]
local bundleIdValue = anim:FindFirstChild("ThumbnailBundleId")
if bundleIdValue and bundleIdValue:IsA("NumberValue") then
bundleId = bundleIdValue.Value
end
end
local character
if bundleId then
-- Thumbnail with the loaded bundle
character = BundleLoader.LoadBundleCharacter(baseUrl, bundleId)
ThumbnailGenerator:AddProfilingCheckpoint("BundleCharacterLoaded")
else
-- Use characterAppearanceUrl
local player = game:GetService("Players"):CreateLocalPlayer(0)
player.CharacterAppearance = characterAppearanceUrl
player:LoadCharacterBlocking()
character = player.Character
ThumbnailGenerator:AddProfilingCheckpoint("PlayerCharacterLoaded")
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
getOriginalJointCFrames(character)
local animator = humanoid:FindFirstChild("Animator")
if not animator then
animator = Instance.new("Animator")
animator.Parent = humanoid
end
if rotateCharacter then
local rootPart = character:FindFirstChild("HumanoidRootPart")
if rootPart then
rootPart.CFrame = rootPart.CFrame * CFrame.Angles(math.rad(-90), 0, 0)
end
end
local animationId = ""
local bestWeight = -1
for _, anim in pairs(animations) do
local weight = anim:FindFirstChild("Weight")
if weight then
if weight.Value > bestWeight then
bestWeight = weight.Value
animationId = anim.AnimationId
end
else
animationId = anim.AnimationId
end
end
local KeyframeSequenceProvider = game:GetService("KeyframeSequenceProvider")
local keyframeSequence = KeyframeSequenceProvider:GetKeyframeSequence(animationId)
local animationObj = Instance.new("Animation")
animationObj.AnimationId = KeyframeSequenceProvider:RegisterActiveKeyframeSequence(keyframeSequence)
for _, track in pairs(humanoid:GetPlayingAnimationTracks()) do
track:Stop(0)
end
local track = humanoid:LoadAnimation(animationObj)
track:Play(0)
ThumbnailGenerator:AddProfilingCheckpoint("AnimationsLoaded")
local finished = false
local function stepNextFrame()
local delta = 1/FRAME_RATE
if track.TimePosition + delta > track.Length then
delta = track.Length - track.TimePosition
end
if delta <= 0.005 or track.TimePosition >= track.Length or finished then
return false
end
local preStepTimePosition = track.TimePosition
animator:StepAnimations(delta)
if track.TimePosition <= preStepTimePosition then
finished = true
end
return true
end
local math_floor = math.floor
local math_sin = math.sin
local math_cos = math.cos
local math_min = math.min
local math_max = math.max
local function round(number)
return math_floor(number*10000 + 0.5)/10000
end
local function vector3ToTable(vector)
return {
x = round(vector.X),
y = round(vector.Y),
z = round(vector.Z)
}
end
local animationData = {}
while stepNextFrame() do
local frameAnimationData = {}
for _, obj in pairs(character:GetChildren()) do
if obj:IsA("BasePart") or obj:IsA("Accoutrement") then
local part = obj
if obj:IsA("Accoutrement") then
part = obj:FindFirstChild("Handle")
end
if part and part.Name ~= "HumanoidRootPart" then
local posAndRotation = {}
posAndRotation["Position"] = vector3ToTable(part.Position)
local axis, angle = part.CFrame:toAxisAngle()
local halfAngle = angle/2
posAndRotation["Rotation"] = {
x = round(math_sin(halfAngle)*axis.X),
y = round(math_sin(halfAngle)*axis.Y),
z = round(math_sin(halfAngle)*axis.Z),
w = round(math_cos(halfAngle))
}
frameAnimationData[obj.Name] = posAndRotation
end
end
end
table.insert(animationData, frameAnimationData)
end
ThumbnailGenerator:AddProfilingCheckpoint("AnimationDataCollected")
-- Take note of finishing CFrames for the animation
-- The CameraResult position will be transformed based on this
local animatedPartPositionsMap = {}
for part, _ in pairs(originalPartsCFramesMap) do
animatedPartPositionsMap[part] = part.Position
end
-- Restore original CFrames
for motor, origC1 in pairs(originalJointCFramesMap) do
motor.C1 = origC1
end
for part, cframe in pairs(originalPartsCFramesMap) do
part.Anchored = true
part.CFrame = cframe
end
local partsArray = {}
for _, obj in pairs(character:GetChildren()) do
if obj:IsA("BasePart") or obj:IsA("Accoutrement") then
if obj.Name ~= "HumanoidRootPart" then
table.insert(partsArray, obj)
end
end
end
local string_sub = string.sub
local string_len = string.len
local function strEndsWith(str, val)
return string_sub(str, -string_len(val)) == val
end
local function replaceChar(pos, str, r)
return str:sub(1, pos - 1) ..r.. str:sub(pos + 1)
end
game:GetService("Selection"):Set(partsArray)
local objsStrOutput, requestedUrls = ThumbnailGenerator:Click("SplitObjs", 0, 0, true)
ThumbnailGenerator:AddProfilingCheckpoint("ObjFilesGenerated")
local decodedObjsStrOutput = game:GetService("HttpService"):JSONDecode(objsStrOutput)
local partObjsResult = {}
local textures = {}
local cameraResult = nil
local totalAABB = {
["min"] = {},
["max"] = {}
}
local function addToTotalAABB(partName, partAABB)
local part = character:FindFirstChild(partName)
if part and animatedPartPositionsMap[part] then
local currentPosition = part.Position
local animatedPosition = animatedPartPositionsMap[part]
local offset = animatedPosition - currentPosition
local minJSON = partAABB["min"]
local currentMin = Vector3.new(minJSON.x, minJSON.y, minJSON.z)
currentMin = Vector3.new(currentMin.X, currentMin.Y + offset.Y, currentMin.Z)
partAABB["min"] = vector3ToTable(currentMin)
local maxJSON = partAABB["min"]
local currentMax = Vector3.new(maxJSON.x, maxJSON.y, maxJSON.z)
currentMax = Vector3.new(currentMax.X, currentMax.Y + offset.Y, currentMax.Z)
partAABB["min"] = vector3ToTable(currentMax)
end
for xyzkey, val in pairs(partAABB["min"]) do
if not totalAABB["min"][xyzkey] then
totalAABB["min"][xyzkey] = val
else
totalAABB["min"][xyzkey] = math_min(totalAABB["min"][xyzkey], val)
end
end
for xyzkey, val in pairs(partAABB["max"]) do
if not totalAABB["max"][xyzkey] then
totalAABB["max"][xyzkey] = val
else
totalAABB["max"][xyzkey] = math_max(totalAABB["max"][xyzkey], val)
end
end
end
local function resolveCameraResult(partName, cameraJSON)
local part = character:FindFirstChild(partName)
if part and animatedPartPositionsMap[part] then
local currentPosition = part.Position
local animatedPosition = animatedPartPositionsMap[part]
local offset = animatedPosition - currentPosition
local positionJSON = cameraJSON["position"]
local cameraPosition = Vector3.new(positionJSON.x, positionJSON.y, positionJSON.z)
cameraPosition = cameraPosition + offset
cameraJSON["position"] = vector3ToTable(cameraPosition)
end
if partName == "Head" then
cameraResult = cameraJSON
elseif cameraResult == nil then -- Fallback if Head doesn't exist for some reason
cameraResult = cameraJSON
end
end
-- Process the SplitObjs output to consolidate it for the animation output.
-- The data for common textures is mapped to the same output in the textures table.
-- The Camera and AABB fields are consolidated into global fields.
for key, val in pairs(decodedObjsStrOutput) do
local decodedPartObj = game:GetService("HttpService"):JSONDecode(val)
if decodedPartObj["files"] then
local files = decodedPartObj["files"]
for fileName, fileInfo in pairs(files) do
local fileContent = fileInfo.content
if strEndsWith(fileName, ".png") then
local newFileName = fileName
while textures[newFileName] and textures[newFileName].content ~= fileContent do
local charsFromEnd = string_len("Tex.png")
local replacePos = string_len(newFileName) - charsFromEnd
local newNumber = tostring(tonumber(newFileName:sub(replacePos, replacePos)) + 1)
newFileName = replaceChar(replacePos, newFileName, newNumber)
end
textures[newFileName] = {
content = fileContent
}
files["texture"] = newFileName
files[fileName] = nil
end
end
end
-- Calculate total aabb
if decodedPartObj["AABB"] then
addToTotalAABB(key, decodedPartObj["AABB"])
decodedPartObj["AABB"] = nil
end
-- Resolve overall Camera JSON.
if decodedPartObj["camera"] then
resolveCameraResult(key, decodedPartObj["camera"])
decodedPartObj["camera"] = nil
end
partObjsResult[key] = decodedPartObj
end
-- Special camera position and direction for rotated character
if rotateCharacter then
local rootPart = character:FindFirstChild("HumanoidRootPart")
if rootPart then
local cameraOffset = Vector3.new(6, 5, 7) * .7
local cameraPosition = Vector3.new(rootPart.Position.X, rootPart.Position.Y, rootPart.Position.Z) - cameraOffset
local cameraDirection = (rootPart.Position - cameraPosition).unit
cameraResult["position"] = vector3ToTable(cameraPosition)
cameraResult["direction"] = vector3ToTable(cameraDirection)
end
end
local resultData = {
Frames = animationData,
Camera = cameraResult,
AABB = totalAABB,
PartObjs = partObjsResult,
Textures = textures
}
ThumbnailGenerator:AddProfilingCheckpoint("ResultFinalized")
return game:GetService("HttpService"):JSONEncode(resultData), requestedUrls