-- 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