public AssemblerData Assemble(UserAvatar avatar) { Contract.Requires(avatar != null); UserInfo userInfo = avatar.UserInfo; string userName = FileUtility.MakeNameWindowsSafe(userInfo.Username); string appData = Environment.GetEnvironmentVariable("LocalAppData"); string rbx2Src = Path.Combine(appData, "Rbx2Source"); string avatars = Path.Combine(rbx2Src, "Avatars"); string userBin = Path.Combine(avatars, userName); string modelDir = Path.Combine(userBin, "Model"); string anim8Dir = Path.Combine(modelDir, "Animations"); string texturesDir = Path.Combine(userBin, "Textures"); string materialsDir = Path.Combine(userBin, "Materials"); FileUtility.InitiateEmptyDirectories(modelDir, anim8Dir, texturesDir, materialsDir); AvatarType avatarType = avatar.PlayerAvatarType; ICharacterAssembler assembler; if (avatarType == AvatarType.R15) { assembler = new R15CharacterAssembler(); } else { assembler = new R6CharacterAssembler(); } string compileDir = "roblox_avatars/" + userName; string avatarTypeName = Rbx2Source.GetEnumName(avatarType); Folder characterAssets = AppendCharacterAssets(avatar, avatarTypeName); Rbx2Source.ScheduleTasks ( "BuildCharacter", "BuildCollisionModel", "BuildAnimations", "BuildTextures", "BuildMaterials", "BuildCompilerScript" ); Rbx2Source.PrintHeader("BUILDING CHARACTER MODEL"); #region Build Character Model /////////////////////////////////////////////////////////////////////////////////////////////////////// StudioMdlWriter writer = assembler.AssembleModel(characterAssets, avatar.Scales, DEBUG_RAPID_ASSEMBLY); string studioMdl = writer.BuildFile(); string modelPath = Path.Combine(modelDir, "CharacterModel.smd"); FileUtility.WriteFile(modelPath, studioMdl); string staticPose = writer.BuildFile(false); string refPath = Path.Combine(modelDir, "ReferencePos.smd"); FileUtility.WriteFile(refPath, staticPose); Rbx2Source.MarkTaskCompleted("BuildCharacter"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("BUILDING COLLISION MODEL"); #region Build Character Collisions /////////////////////////////////////////////////////////////////////////////////////////////////////// Folder collisionAssets = AppendCollisionAssets(avatar, avatarTypeName); StudioMdlWriter collisionWriter = assembler.AssembleModel(collisionAssets, avatar.Scales, true); string collisionModel = collisionWriter.BuildFile(); string cmodelPath = Path.Combine(modelDir, "CollisionModel.smd"); FileUtility.WriteFile(cmodelPath, collisionModel); byte[] collisionJoints = assembler.CollisionModelScript; string cjointsPath = Path.Combine(modelDir, "CollisionJoints.qc"); FileUtility.WriteFile(cjointsPath, collisionJoints); Rbx2Source.MarkTaskCompleted("BuildCollisionModel"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("BUILDING CHARACTER ANIMATIONS"); #region Build Character Animations /////////////////////////////////////////////////////////////////////////////////////////////////////// var animIds = assembler.CollectAnimationIds(avatar); var compileAnims = new Dictionary <string, Asset>(); if (animIds.Count > 0) { Rbx2Source.Print("Collecting Animations..."); Rbx2Source.IncrementStack(); Action <string, Asset> collectAnimation = (animName, animAsset) => { if (!compileAnims.ContainsKey(animName)) { Rbx2Source.Print("Collected animation {0} with id {1}", animName, animAsset.Id); compileAnims.Add(animName, animAsset); } }; foreach (string animName in animIds.Keys) { var animId = animIds[animName]; var animAsset = animId.GetAsset(); var import = animAsset.OpenAsModel(); if (animId.AnimationType == AnimationType.R15AnimFolder) { Folder r15Anim = import.FindFirstChild <Folder>("R15Anim"); if (r15Anim != null) { foreach (Instance animDef in r15Anim.GetChildren()) { if (animDef.Name == "idle") { var anims = animDef.GetChildrenOfType <Animation>(); if (anims.Length == 2) { var getLookAnim = anims.OrderBy((anim) => { var weight = anim.FindFirstChild <NumberValue>("Weight"); if (weight != null) { return(weight.Value); } return(0.0); }); var lookAnim = getLookAnim.First(); lookAnim.Destroy(); Asset lookAsset = Asset.GetByAssetId(lookAnim.AnimationId); collectAnimation("Idle2", lookAsset); } } Animation compileAnim = animDef.FindFirstChildOfClass <Animation>(); if (compileAnim != null) { Asset compileAsset = Asset.GetByAssetId(compileAnim.AnimationId); string compileName = animName; if (animDef.Name == "pose") { compileName = "Pose"; } collectAnimation(compileName, compileAsset); } } } } else { collectAnimation(animName, animAsset); } } Rbx2Source.DecrementStack(); } else { Rbx2Source.Print("No animations found :("); } if (compileAnims.Count > 0) { Rbx2Source.Print("Assembling Animations..."); Rbx2Source.IncrementStack(); foreach (string animName in compileAnims.Keys) { Rbx2Source.Print("Building Animation {0}...", animName); Asset animAsset = compileAnims[animName]; var import = animAsset.OpenAsModel(); var sequence = import.FindFirstChildOfClass <KeyframeSequence>(); sequence.Name = animName; var avatarTypeRef = new StringValue() { Value = $"{avatarType}", Name = "AvatarType", Parent = sequence }; string animation = AnimationBuilder.Assemble(sequence, writer.Skeleton[0].Bones); string animPath = Path.Combine(anim8Dir, animName + ".smd"); FileUtility.WriteFile(animPath, animation); } Rbx2Source.DecrementStack(); } Rbx2Source.MarkTaskCompleted("BuildAnimations"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("BUILDING CHARACTER TEXTURES"); #region Build Character Textures /////////////////////////////////////////////////////////////////////////////////////////////////////// var materials = writer.Materials; TextureBindings textures; if (DEBUG_RAPID_ASSEMBLY) { textures = new TextureBindings(); materials.Clear(); } else { TextureCompositor texCompositor = assembler.ComposeTextureMap(characterAssets, avatar.BodyColors); textures = assembler.BindTextures(texCompositor, materials); } var images = textures.Images; textures.MaterialDirectory = compileDir; foreach (string imageName in images.Keys) { Rbx2Source.Print("Writing Image {0}.png", imageName); Image image = images[imageName]; string imagePath = Path.Combine(texturesDir, imageName + ".png"); try { image.Save(imagePath, ImageFormat.Png); } catch { Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName); } FileUtility.LockFile(imagePath); } CompositData.FreeAllocatedTextures(); Rbx2Source.MarkTaskCompleted("BuildTextures"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("WRITING MATERIAL FILES"); #region Write Material Files /////////////////////////////////////////////////////////////////////////////////////////////////////// var matLinks = textures.MatLinks; foreach (string mtlName in matLinks.Keys) { Rbx2Source.Print("Building VMT {0}.vmt", mtlName); string targetVtf = matLinks[mtlName]; string vmtPath = Path.Combine(materialsDir, mtlName + ".vmt"); ValveMaterial mtl = materials[mtlName]; mtl.SetVmtField("basetexture", "models/" + compileDir + "/" + targetVtf); mtl.WriteVmtFile(vmtPath); } Rbx2Source.MarkTaskCompleted("BuildMaterials"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT"); #region Write Compiler Script /////////////////////////////////////////////////////////////////////////////////////////////////////// string modelName = compileDir + ".mdl"; QuakeCWriter qc = new QuakeCWriter(); qc.Add("body", userName, "CharacterModel.smd"); qc.Add("modelname", modelName); qc.Add("upaxis", "y"); // Compute the floor level of the avatar. Folder assembly = characterAssets.FindFirstChild <Folder>("ASSEMBLY"); if (assembly != null) { float floor = ComputeFloorLevel(assembly); string origin = "0 " + floor.ToInvariantString() + " 0"; qc.Add("origin", origin); } qc.Add("cdmaterials", "models/" + compileDir); qc.Add("surfaceprop", "flesh"); qc.Add("include", "CollisionJoints.qc"); QuakeCItem refAnim = qc.Add("sequence", "reference", "ReferencePos.smd"); refAnim.AddSubItem("fps", 1); refAnim.AddSubItem("loop"); foreach (string animName in compileAnims.Keys) { QuakeCItem sequence = qc.Add("sequence", animName.ToLowerInvariant(), "Animations/" + animName + ".smd"); sequence.AddSubItem("fps", AnimationBuilder.FrameRate); if (avatarType == AvatarType.R6) { sequence.AddSubItem("delta"); } sequence.AddSubItem("loop"); } string qcFile = qc.ToString(); string qcPath = Path.Combine(modelDir, "Compile.qc"); FileUtility.WriteFile(qcPath, qcFile); Rbx2Source.MarkTaskCompleted("BuildCompilerScript"); /////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion AssemblerData data = new AssemblerData() { ModelData = writer, ModelName = modelName, TextureData = textures, CompilerScript = qcPath, RootDirectory = userBin, CompileDirectory = compileDir, TextureDirectory = texturesDir, MaterialDirectory = materialsDir, }; return(data); }
public static string Assemble(KeyframeSequence sequence, List <Bone> rig) { StudioMdlWriter animWriter = new StudioMdlWriter(); List <Keyframe> keyframes = new List <Keyframe>(); Dictionary <string, Bone> boneLookup = new Dictionary <string, Bone>(); List <Node> nodes = animWriter.Nodes; foreach (Bone bone in rig) { Node node = bone.Node; if (node != null) { nodes.Add(node); string boneName = node.Name; if (!boneLookup.ContainsKey(boneName)) { boneLookup.Add(boneName, bone); } } } foreach (Keyframe kf in sequence.GetChildrenOfClass <Keyframe>()) { Pose rootPart = kf.FindFirstChild <Pose>("HumanoidRootPart"); if (rootPart != null) { // We don't need the rootpart for this. foreach (Pose subPose in rootPart.GetChildrenOfClass <Pose>()) { subPose.Parent = kf; } rootPart.Destroy(); } kf.Time /= sequence.TimeScale; keyframes.Add(kf); } keyframes.Sort(0, keyframes.Count, sorter); Keyframe lastKeyframe = keyframes[keyframes.Count - 1]; float fLength = lastKeyframe.Time; int frameCount = ToFrameRate(fLength); // Animations in source are kinda dumb, because theres no frame interpolation. // I have to account for every single CFrame for every single frame. Dictionary <int, Dictionary <string, Pose> > keyframeMap = new Dictionary <int, Dictionary <string, Pose> >(); for (int i = 0; i <= frameCount; i++) { keyframeMap[i] = new Dictionary <string, Pose>(); } foreach (Keyframe kf in keyframes) { int frame = ToFrameRate(kf.Time); Dictionary <string, Pose> poseMap = keyframeMap[frame]; List <Pose> poses = GatherPoses(kf); foreach (Pose pose in poses) { poseMap[pose.Name] = pose; } } List <BoneKeyframe> boneKeyframes = animWriter.Skeleton; Keyframe baseFrame = keyframes[0]; for (int i = 0; i < frameCount; i++) { BoneKeyframe frame = new BoneKeyframe(); frame.Time = i; if (sequence.AvatarType == AvatarType.R15) { frame.DeltaSequence = true; frame.BaseRig = rig; } List <Bone> bones = frame.Bones; foreach (Node node in nodes) { PosePair closestPoses = GetClosestPoses(keyframeMap, i, node.Name); float current = i; float min = closestPoses.Min.Frame; float max = closestPoses.Max.Frame; float alpha = (min == max ? 0 : (current - min) / (max - min)); Pose pose0 = closestPoses.Min.Pose; Pose pose1 = closestPoses.Max.Pose; float weight = EasingUtil.GetEasing(pose1.PoseEasingStyle, pose1.PoseEasingDirection, 1 - alpha); CFrame lastCFrame = pose0.CFrame; CFrame nextCFrame = pose1.CFrame; Bone baseBone = boneLookup[node.Name]; CFrame c0 = baseBone.C0; CFrame c1 = baseBone.C1; CFrame interp = lastCFrame.lerp(nextCFrame, weight); // some ugly manual fixes. // todo: make this unnecessary :( if (sequence.AvatarType == AvatarType.R6) { Vector3 pos = interp.p; CFrame rot = interp - pos; if (node.Name == "Torso") { float[] ang = interp.toEulerAnglesXYZ(); rot = CFrame.Angles(ang[0], ang[2], ang[1]); pos = new Vector3(pos.x, pos.z, pos.y); } else if (node.Name.StartsWith("Right")) { pos *= new Vector3(-1, 1, 1); } if (node.Name.Contains("Arm") || node.Name.Contains("Leg")) { pos = new Vector3(pos.z, pos.y, pos.x); } if (sequence.Name == "Climb" && node.Name.Contains("Leg")) // https://www.youtube.com/watch?v=vfJ7DqyDl9w { pos += new Vector3(-.1f, 0, 0); } interp = new CFrame(pos) * rot; } else if (sequence.AvatarType == AvatarType.R15) { if (node.Name.Contains("UpperArm")) { Vector3 pos = interp.p; CFrame rot = interp - pos; float[] ang = rot.toEulerAnglesXYZ(); if (sequence.Name == "Climb" || sequence.Name == "Swim") { rot = CFrame.Angles(ang[0], -ang[2], -ang[1]); } interp = new CFrame(pos) * rot; } } Bone bone = new Bone(node.Name, i, interp); bone.Node = node; bones.Add(bone); } boneKeyframes.Add(frame); } return(animWriter.BuildFile()); }
public static string Assemble(KeyframeSequence sequence, List <Bone> rig) { StudioMdlWriter animWriter = new StudioMdlWriter(); List <Keyframe> keyframes = new List <Keyframe>(); var boneLookup = new Dictionary <string, Bone>(); var nodes = animWriter.Nodes; foreach (Bone bone in rig) { Node node = bone.Node; if (node != null) { string boneName = node.Name; if (!boneLookup.ContainsKey(boneName)) { boneLookup.Add(boneName, bone); } nodes.Add(node); } } foreach (Keyframe kf in sequence.GetChildrenOfClass <Keyframe>()) { Pose rootPart = kf.FindFirstChild <Pose>("HumanoidRootPart"); if (rootPart != null) { // We don't need the rootpart for this. foreach (Pose subPose in rootPart.GetChildrenOfClass <Pose>()) { subPose.Parent = kf; } rootPart.Destroy(); } kf.Time /= sequence.TimeScale; keyframes.Add(kf); } keyframes.Sort(0, keyframes.Count, sorter); Keyframe lastKeyframe = keyframes[keyframes.Count - 1]; float fLength = lastKeyframe.Time; int frameCount = ToFrameRate(fLength); // As far as I can tell, models in Source require you to store poses for every // single frame, so I need to fill in the gaps with interpolated pose CFrames. var keyframeMap = new Dictionary <int, Dictionary <string, Pose> >(); foreach (Keyframe kf in keyframes) { int frame = ToFrameRate(kf.Time); var poses = GatherPoses(kf); var poseMap = poses.ToDictionary(pose => pose.Name); keyframeMap[frame] = poseMap; } // Make sure there are no holes in the data. for (int i = 0; i < frameCount; i++) { if (!keyframeMap.ContainsKey(i)) { var emptyState = new Dictionary <string, Pose>(); keyframeMap.Add(i, emptyState); } } List <BoneKeyframe> boneKeyframes = animWriter.Skeleton; for (int i = 0; i < frameCount; i++) { BoneKeyframe frame = new BoneKeyframe(i); List <Bone> bones = frame.Bones; if (sequence.AvatarType == AvatarType.R15) { frame.BaseRig = rig; frame.DeltaSequence = true; } foreach (Node node in nodes) { PosePair closestPoses = GetClosestPoses(keyframeMap, i, node.Name); float min = closestPoses.Min.Frame; float max = closestPoses.Max.Frame; float alpha = (min == max ? 0 : (i - min) / (max - min)); Pose pose0 = closestPoses.Min.Pose; Pose pose1 = closestPoses.Max.Pose; CFrame lastCFrame = pose0.CFrame; CFrame nextCFrame = pose1.CFrame; Bone baseBone = boneLookup[node.Name]; CFrame interp = lastCFrame.Lerp(nextCFrame, alpha); // Make some patches to the interpolation offsets. Unfortunately I can't // identify any single fix that I can apply to each joint, so I have to get crafty. // At some point in the future, I want to find a more practical solution for this problem, // but it is extremely difficult to isolate if any single solution exists. if (sequence.AvatarType == AvatarType.R6) { Vector3 pos = interp.Position; CFrame rot = interp - pos; if (node.Name == "Torso") { // Flip the YZ axis of the Torso. float[] ang = interp.ToEulerAnglesXYZ(); rot = CFrame.Angles(ang[0], ang[2], ang[1]); pos = new Vector3(pos.X, pos.Z, pos.Y); } else if (node.Name.StartsWith("Right")) { // X-axis is inverted for the right arm/leg. pos *= new Vector3(-1, 1, 1); } if (node.Name.EndsWith("Arm") || node.Name.EndsWith("Leg")) { // Rotate position offset of the arms & legs 90* counter-clockwise. pos = new Vector3(-pos.Z, pos.Y, pos.X); } if (node.Name != "Head") { rot = rot.Inverse(); } interp = new CFrame(pos) * rot; } else if (sequence.AvatarType == AvatarType.R15) { float[] ang = interp.ToEulerAnglesXYZ(); // Cancel out the rotations interp *= CFrame.Angles(-ang[0], -ang[1], -ang[2]); // Patch the Y-axis PatchAngles(ref interp, 1, ang); // Patch the Z-axis PatchAngles(ref interp, 2, ang); // Patch the X-axis PatchAngles(ref interp, 0, ang); } Bone bone = new Bone(node, interp); bones.Add(bone); } boneKeyframes.Add(frame); } return(animWriter.BuildFile()); }
public AssemblerData Assemble(long assetId) { Asset asset = Asset.Get(assetId); string assetName = asset.ProductInfo.WindowsSafeName.Trim(); string appData = Environment.GetEnvironmentVariable("LocalAppData"); string rbx2Source = Path.Combine(appData, "Rbx2Source"); string items = Path.Combine(rbx2Source, "Items"); string rootDir = Path.Combine(items, assetName); string modelDir = Path.Combine(rootDir, "Model"); string texturesDir = Path.Combine(rootDir, "Textures"); string materialsDir = Path.Combine(rootDir, "Materials"); FileUtility.InitiateEmptyDirectories(modelDir, texturesDir, materialsDir); Rbx2Source.ScheduleTasks("BuildModel", "BuildTextures", "BuildMaterials", "BuildCompilerScript"); Rbx2Source.PrintHeader("BUILDING MODEL"); #region Build Model /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// StudioMdlWriter writer = AssembleModel(asset); string studioMdl = writer.BuildFile(); string modelPath = Path.Combine(modelDir, "Asset.smd"); FileUtility.WriteFile(modelPath, studioMdl); string reference = writer.BuildFile(false); string refPath = Path.Combine(modelDir, "Reference.smd"); FileUtility.WriteFile(refPath, reference); Rbx2Source.MarkTaskCompleted("BuildModel"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("BUILDING TEXTURES"); #region Build Textures /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// var materials = writer.Materials; var textures = BindTextures(materials); var images = textures.Images; var compileDir = "roblox_assets/" + assetName; foreach (string imageName in images.Keys) { Rbx2Source.Print("Writing Image {0}", imageName); Image image = images[imageName]; string imagePath = Path.Combine(texturesDir, imageName + ".png"); try { image.Save(imagePath, ImageFormat.Png); } catch { Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName); } FileUtility.LockFile(imagePath); } textures.MaterialDirectory = compileDir; Rbx2Source.MarkTaskCompleted("BuildTextures"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("WRITING MATERIAL FILES"); #region Write Materials /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// string mtlDir = "models/" + compileDir; var matLinks = textures.MatLinks; foreach (string matName in matLinks.Keys) { string vtfTarget = matLinks[matName]; string vmtPath = Path.Combine(materialsDir, matName + ".vmt"); ValveMaterial mat = materials[matName]; mat.SetVmtField("basetexture", mtlDir + '/' + vtfTarget); mat.WriteVmtFile(vmtPath); } Rbx2Source.MarkTaskCompleted("BuildMaterials"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT"); #region Write Compiler Script /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// string modelName = compileDir + ".mdl"; QuakeCWriter qc = new QuakeCWriter(); qc.Add("body", assetName, "Asset.smd"); qc.Add("modelname", modelName); qc.Add("upaxis", "y"); qc.Add("cdmaterials", mtlDir); QuakeCItem phys = qc.Add("collisionjoints", "Asset.smd"); phys.AddSubItem("$mass", 115.0); phys.AddSubItem("$inertia", 2.00); phys.AddSubItem("$damping", 0.01); phys.AddSubItem("$rotdamping", 0.40); QuakeCItem refAnim = qc.Add("sequence", "reference", "Reference.smd"); refAnim.AddSubItem("fps", 1); refAnim.AddSubItem("loop"); string qcFile = qc.ToString(); string qcPath = Path.Combine(modelDir, "Compile.qc"); FileUtility.WriteFile(qcPath, qcFile); Rbx2Source.MarkTaskCompleted("BuildCompilerScript"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #endregion AssemblerData data = new AssemblerData() { ModelData = writer, ModelName = modelName, TextureData = textures, CompilerScript = qcPath, RootDirectory = rootDir, CompileDirectory = compileDir, TextureDirectory = texturesDir, MaterialDirectory = materialsDir }; return(data); }
public AssemblerData Assemble(object metadata) { UserAvatar avatar = metadata as UserAvatar; if (avatar == null) { throw new Exception("bad cast"); } UserInfo userInfo = avatar.UserInfo; string userName = FileUtility.MakeNameWindowsSafe(userInfo.Username); string appData = Environment.GetEnvironmentVariable("AppData"); string rbx2Source = Path.Combine(appData, "Rbx2Source"); string avatars = Path.Combine(rbx2Source, "Avatars"); string userBin = Path.Combine(avatars, userName); string modelDir = Path.Combine(userBin, "Model"); string animDir = Path.Combine(modelDir, "Animations"); string texturesDir = Path.Combine(userBin, "Textures"); string materialsDir = Path.Combine(userBin, "Materials"); FileUtility.InitiateEmptyDirectories(modelDir, animDir, texturesDir, materialsDir); AvatarType avatarType = avatar.ResolvedAvatarType; ICharacterAssembler assembler; if (avatarType == AvatarType.R15) { assembler = new R15CharacterAssembler(); } else { assembler = new R6CharacterAssembler(); } string avatarTypeName = Rbx2Source.GetEnumName(avatar.ResolvedAvatarType); Folder characterAssets = AppendCharacterAssets(avatar, avatarTypeName); Rbx2Source.ScheduleTasks("BuildCharacter", "BuildCollisionModel", "BuildAnimations", "BuildTextures", "BuildMaterials", "BuildCompilerScript"); Rbx2Source.PrintHeader("BUILDING CHARACTER MODEL"); StudioMdlWriter writer = assembler.AssembleModel(characterAssets, avatar.Scales); string studioMdl = writer.BuildFile(); string modelPath = Path.Combine(modelDir, "CharacterModel.smd"); FileUtility.WriteFile(modelPath, studioMdl); // Clear the triangles so we can build a reference pose .smd file. writer.Triangles.Clear(); string staticPose = writer.BuildFile(); string refPath = Path.Combine(modelDir, "ReferencePos.smd"); FileUtility.WriteFile(refPath, staticPose); Rbx2Source.MarkTaskCompleted("BuildCharacter"); Rbx2Source.PrintHeader("BUILDING COLLISION MODEL"); Folder lowPoly = new Folder(); SpecialMesh lowPolyHead = new SpecialMesh(); lowPolyHead.MeshId = "rbxassetid://582002794"; lowPolyHead.MeshType = MeshType.FileMesh; lowPolyHead.Scale = new Vector3(1, 1, 1); lowPolyHead.Offset = new Vector3(); lowPolyHead.Parent = lowPoly; StudioMdlWriter collisionWriter = assembler.AssembleModel(lowPoly, avatar.Scales); string collisionModel = collisionWriter.BuildFile(); string cmodelPath = Path.Combine(modelDir, "CollisionModel.smd"); FileUtility.WriteFile(cmodelPath, collisionModel); byte[] collisionJoints = assembler.CollisionModelScript; string cjointsPath = Path.Combine(modelDir, "CollisionJoints.qc"); FileUtility.WriteFile(cjointsPath, collisionJoints); Rbx2Source.MarkTaskCompleted("BuildCollisionModel"); Rbx2Source.PrintHeader("BUILDING CHARACTER ANIMATIONS"); Dictionary <string, string> animations = GatherAnimations(avatarType); if (animations.Count > 0) { foreach (string animName in animations.Keys) { Rbx2Source.Print("Building Animation {0}", animName); string localAnimPath = animations[animName]; Asset animAsset = Asset.FromResource(localAnimPath); Folder import = RBXM.LoadFromAsset(animAsset); KeyframeSequence sequence = import.FindFirstChildOfClass <KeyframeSequence>(); sequence.Name = animName; sequence.AvatarType = avatarType; string animation = AnimationAssembler.Assemble(sequence, writer.Skeleton[0].Bones); string animPath = Path.Combine(animDir, animName + ".smd"); FileUtility.WriteFile(animPath, animation); } } else { Rbx2Source.Print("No animations found :("); } Rbx2Source.MarkTaskCompleted("BuildAnimations"); Dictionary <string, Material> materials = writer.Materials; Rbx2Source.PrintHeader("BUILDING CHARACTER TEXTURES"); string compileDirectory = "roblox_avatars/" + userName; TextureCompositor texCompositor = assembler.ComposeTextureMap(characterAssets, avatar.BodyColors); TextureAssembly texAssembly = assembler.AssembleTextures(texCompositor, materials); CompositData.FreeAllocatedTextures(); texAssembly.MaterialDirectory = compileDirectory; Dictionary <string, Image> images = texAssembly.Images; foreach (string imageName in images.Keys) { Rbx2Source.Print("Writing Image {0}.png", imageName); Image image = images[imageName]; string imagePath = Path.Combine(texturesDir, imageName + ".png"); try { image.Save(imagePath, ImageFormat.Png); } catch { Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName); } FileUtility.LockFile(imagePath); } Rbx2Source.MarkTaskCompleted("BuildTextures"); Rbx2Source.PrintHeader("BUILDING MATERIAL FILES"); Dictionary <string, string> matLinks = texAssembly.MatLinks; foreach (string mtlName in matLinks.Keys) { Rbx2Source.Print("Building VMT {0}.vmt", mtlName); string targetVtf = matLinks[mtlName]; Material mtl = materials[mtlName]; ValveMaterial vmt = new ValveMaterial(mtl); vmt.SetField("basetexture", "models/" + compileDirectory + "/" + targetVtf); string vmtPath = Path.Combine(materialsDir, mtlName + ".vmt"); string vmtContent = vmt.ToString(); FileUtility.WriteFile(vmtPath, vmtContent); } Rbx2Source.MarkTaskCompleted("BuildMaterials"); Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT"); QCWriter qc = new QCWriter(); QCommand model = new QCommand("body", userName, "CharacterModel.smd"); qc.AddCommand(model); string modelNameStr = compileDirectory + ".mdl"; qc.WriteBasicCmd("modelname", modelNameStr); qc.WriteBasicCmd("upaxis", "y"); string originStr = ""; if (avatarType == AvatarType.R6) { originStr = "0 -30 0"; } else { originStr = "0 " + (-23.5 * avatar.Scales.Height).ToString(Rbx2Source.NormalParse) + " 0"; } qc.WriteBasicCmd("origin", originStr, false); qc.WriteBasicCmd("cdmaterials", "models/" + compileDirectory); qc.WriteBasicCmd("surfaceprop", "flesh"); qc.WriteBasicCmd("include", "CollisionJoints.qc"); QCommand reference = new QCommand("sequence", "reference", "ReferencePos.smd"); reference.AddParameter("fps", "1"); reference.AddParameter("loop"); qc.AddCommand(reference); foreach (string animName in animations.Keys) { QCommand sequence = new QCommand("sequence", animName.ToLower(), "Animations/" + animName + ".smd"); sequence.AddParameter("fps", AnimationAssembler.FrameRate.ToString()); sequence.AddParameter("loop"); if (avatarType == AvatarType.R6) // TODO: Find a work around so I can get rid of this. { sequence.AddParameter("delta"); } qc.AddCommand(sequence); } string qcFile = qc.BuildFile(); string qcPath = Path.Combine(modelDir, "Compile.qc"); FileUtility.WriteFile(qcPath, qcFile); Rbx2Source.MarkTaskCompleted("BuildCompilerScript"); AssemblerData data = new AssemblerData(); data.ModelData = writer; data.TextureData = texAssembly; data.CompilerScript = qcPath; data.RootDirectory = userBin; data.MaterialDirectory = materialsDir; data.TextureDirectory = texturesDir; data.CompileDirectory = compileDirectory; data.ModelName = modelNameStr; return(data); }
public AssemblerData Assemble(object metadata) { long assetId = (long)metadata; Asset asset = Asset.Get(assetId); string assetName = asset.ProductInfo.WindowsSafeName; string appData = Environment.GetEnvironmentVariable("AppData"); string rbx2Source = Path.Combine(appData, "Rbx2Source"); string items = Path.Combine(rbx2Source, "Items"); string rootDir = Path.Combine(items, assetName); string modelDir = Path.Combine(rootDir, "Model"); string texturesDir = Path.Combine(rootDir, "Textures"); string materialsDir = Path.Combine(rootDir, "Materials"); FileUtility.InitiateEmptyDirectories(modelDir, texturesDir, materialsDir); Rbx2Source.ScheduleTasks("BuildModel", "BuildTextures", "BuildMaterials", "BuildCompilerScript"); // Build Model StudioMdlWriter writer = AssembleModel(asset); string studioMdl = writer.BuildFile(); string modelPath = Path.Combine(modelDir, "Asset.smd"); FileUtility.WriteFile(modelPath, studioMdl); // Build Reference Sequence Triangle[] triangles = writer.Triangles.ToArray(); writer.Triangles.Clear(); string reference = writer.BuildFile(); string refPath = Path.Combine(modelDir, "Reference.smd"); FileUtility.WriteFile(refPath, reference); Rbx2Source.MarkTaskCompleted("BuildModel"); // Build Textures Rbx2Source.PrintHeader("BUILDING TEXTURES"); Dictionary <string, Material> materials = writer.Materials; string compileDirectory = "roblox_assets/" + assetName; TextureAssembly texAssembly = AssembleTextures(materials); texAssembly.MaterialDirectory = compileDirectory; Dictionary <string, Image> images = texAssembly.Images; foreach (string imageName in images.Keys) { Rbx2Source.Print("Writing Image {0}", imageName); Image image = images[imageName]; string imagePath = Path.Combine(texturesDir, imageName + ".png"); try { image.Save(imagePath, ImageFormat.Png); } catch { Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName); } FileUtility.LockFile(imagePath); } Rbx2Source.MarkTaskCompleted("BuildTextures"); // Build Materials Rbx2Source.PrintHeader("BUILDING MATERIAL FILES"); string mtlDir = "models/" + compileDirectory; Dictionary <string, string> matLinks = texAssembly.MatLinks; Dictionary <string, Material> matLookup = new Dictionary <string, Material>(); foreach (string mtlName in matLinks.Keys) { Material mtl = materials[mtlName]; string vtfTarget = matLinks[mtlName]; string vmtPath = Path.Combine(materialsDir, mtlName + ".vmt"); if (!File.Exists(vmtPath)) { Rbx2Source.Print("Building VMT {0}.vmt", mtlName); ValveMaterial vmt = new ValveMaterial(mtl); vmt.SetField("basetexture", mtlDir + "/" + vtfTarget); string vmtContent = vmt.ToString(); FileUtility.WriteFile(vmtPath, vmtContent); matLookup[mtlName] = mtl; } } Rbx2Source.MarkTaskCompleted("BuildMaterials"); // Build Compiler Script Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT"); QCWriter qc = new QCWriter(); QCommand model = new QCommand("body", assetName, "Asset.smd"); qc.AddCommand(model); string modelNameStr = compileDirectory + ".mdl"; qc.WriteBasicCmd("modelname", modelNameStr); qc.WriteBasicCmd("upaxis", "y"); qc.WriteBasicCmd("cdmaterials", mtlDir); QCommand collision = new QCommand("collisionjoints", "Asset.smd"); collision.AddParameter("$mass", 115.0); collision.AddParameter("$inertia", 2.00); collision.AddParameter("$damping", 0.01); collision.AddParameter("$rotdamping", 0.40); qc.AddCommand(collision); QCommand sequence = new QCommand("sequence", "reference", "Reference.smd"); sequence.AddParameter("fps", 1); sequence.AddParameter("loop"); qc.AddCommand(sequence); string qcFile = qc.BuildFile(); string qcPath = Path.Combine(modelDir, "Compile.qc"); FileUtility.WriteFile(qcPath, qcFile); Rbx2Source.MarkTaskCompleted("BuildCompilerScript"); AssemblerData data = new AssemblerData(); data.ModelData = writer; data.TextureData = texAssembly; data.CompilerScript = qcPath; data.RootDirectory = rootDir; data.MaterialDirectory = materialsDir; data.TextureDirectory = texturesDir; data.CompileDirectory = compileDirectory; data.ModelName = modelNameStr; return(data); }