diff --git a/Loose Files/ActorXAnimConverter.ms b/Loose Files/ActorXAnimConverter.ms new file mode 100644 index 0000000..0399238 --- /dev/null +++ b/Loose Files/ActorXAnimConverter.ms @@ -0,0 +1,3041 @@ +/* + + ActorX mesh (psk) and animation (psa) importer for 3ds Max + + Created: September 18 2009 + + Author: Konstantin Nosov (aka Gildor) + + Web page: http://www.gildor.org/projects/unactorx + + Revision History: + + 13.10.2019 v1.38 + - implemented loading of vertex colors + + 28.08.2019 v1.37 + - implemented loading of PNG textures + + 19.05.2018 v1.36 + - using case-insensitive comparison when finding bones in scene for animation + + 06.12.2017 v1.35 + - an attempt to make smoothing groups working + - renamed "recurse" option to "look in subfolders" to be less confising to new users + + 16.10.2017 v1.34 + - added possibility to select and open multiple psk files at time + + 21.07.2015 v1.33 + - saving bind pose information inside bone objects, this information will survive saving scene + to a max file + + 18.07.2015 v1.32 + - allowing psa import to work with any mesh, i.e. without previously imported psk file + + 14.07.2015 v1.31 + - trying to detect and repair bad vertex weights for imported psk file + + 01.02.2015 v1.30 + - added animation import option "import at slider position" + - integrated patch by Ayrshi (http://www.gildor.org/smf/index.php/topic,1925.0.html) intended to + improve mesh normals + + 02.12.2014 v1.29 + - ActorX Imported now could be bound to toolbar, keyboard or menu - use "Customize user interface", + category "Gildor Tools", and then use "ActorX Importer" as you like + - reordered controls, separated some options to own rollouts for easy reordering etc + - preserving dialog position, scroll position and rollout "open" state during 3ds Max session (until + Max closed) + + 28.11.2014 v1.28 + - added "mesh translation" options to settings + - "advanced settings" are not stored to the ini file anymore + + 16.12.2013 v1.27 + - added option "Don't conjugate root bone" + + 10.06.2012 v1.26 + - stability improvements + more info: MaxScript documentation, "Do not use return, break, exit or continue" + + 02.06.2012 v1.25 + - fixed Max 2013 support; fix made by sunnydavis, check + http://www.gildor.org/smf/index.php/topic,1408.0.html for details + + 18.02.2012 v1.24 + - fixed parsing psa config file with spaces in track names + + 07.02.2012 v1.23 + - support for extra UV sets stored in standard psk format (ActorX 2010) + + 23.01.2012 v1.22 + - fixed automatic loading of DDS textures for materials + + 06.12.2011 v1.21 + - fixed "translation mode" checkbox to work with psa without config file + + 01.12.2011 v1.20 + - implemented loading of DDS textures + + 26.11.2011 v1.19 + - implemented support for loading pskx files with more than 64k vertices + - added option to control behaviour of animation with rotation-only tracks: you can let AnimSet + to decide which bones will use animated translation, you can force to use translation from the + animation (old, pre-1.18 behaviour) or force to not use animated translation at all; the option + is located in "Animation import" group + + 09.11.2011 v1.18 + - implemented support for animation tracks without translation keys + - reading extended psa information from the .config file, removed psax ANIMFLAGS section support + + 06.11.2011 v1.17 + - eliminated error messages when loading psk or psa file with unknown section name (SCALEKEYS etc) + - implemented support for pskx with 2 or more UV channels + + 03.05.2011 v1.16 + - improved animation cleanup + + 01.01.2011 v1.15 + - workaround for loading animation with the root bone name different than mesh root bone + - removed "Load confirmation" setting (not needed anymore because of functional "batch export") + + 29.12.2010 v1.14 + - added "Batch export" tool + + 22.12.2010 v1.13 + - mesh rotation formula is now identical to used in UnrealEd + - added "Clear scene" tool + + 15.12.2010 v1.12 + - added mesh rotation settings + - added protection from errors appeared when updating this script while 3ds Max is running + + 09.09.2010 v1.11 + - added "reorient bones" option + + 23.07.2010 v1.10 + - implemented extended ActorX format (pskx and psax) support + - "tools" rollout with options to restore mesh bindpose and remove animations + + 24.04.2010 v1.09 + - applying normalmap using correct technique (previously was a bumpmap) + + 14.04.2010 v1.08 + - fixed loading of psk files with root bone parent set to -1 (usually it is 0) + + 20.02.2010 v1.07 + - added "Load confirmation" setting to display message box after completion of operation + - added "Reposition existing bones" option + - fixed error when loading .mat files with missing textures + + 12.12.2009 v1.06 + - fixed merging meshes on a single skeleton when previously loaded mesh is not in bind + pose + - improved compatibility with Epic's ActorX Exporter (dropping trailing spaces from + bone names) + + 18.09.2009 v1.05 + - implemented materal loading + - fixing duplicate bone names + + 29.09.2009 v1.04 + - implemented support for loading non-skeletal (static) meshes + + 26.09.2009 v1.03 + - fixed bug with interpolation between first two animation keyframes + - option to fix animation looping (duplicate first animation frame after last frame) + - added button to load all animations from psa file + - progress bar for loading animation with "cancel" capabilities + - option to not load mesh skin (load skeleton only) + - storing last used directory separately for psk and psa + + 25.09.2009 v1.02 + - added option to scale mesh and animations when loading + - added options for texture search (path, recursive search) + - added option to ask for missing texture files when mesh is loading + + 24.09.2009 v1.01 + - fixed bug in a vertex weighting code + - saving settings to ActorXImporter.ini (Max 9 and higher) + - saving last used psk/psa directory + - settings to change bone size for a new mesh + + 22.09.2009 v1.00 + - first public release + +*/ + + +/* +TODO: +- option to create separate materials, not submaterials +- do not create material when it is already exists - but how to find whether I need to get loaded material + or create a new one? +*/ + +/* +NOTES: +- setBoneEnable false 0: + This call is required. Without it, we will have numerous problems with imported skeleton. Note that FBX + importer will enable "bones" mode, however we can't use it in Max. Why "bone" mode could be useful: hiding + bones could not hide the skeleton when bones are off, and works well when they are on. Why "bone" mode should + be disabled - otherwise we've got problems with rotation/moving of particular bones, they behave like + connected objects. Also saw a bug with imported animation with "force AnimSet translation" - mesh could became + bronen in parts, but began to behave well when unlinked child bone of bad bone and relinked it back. So it's + easier to disable bone mode than to fight against all the bugs. +*/ + +-- constant used to detect ActorX Importer updates during single 3ds Max session +global AX_IMPORTER_VERSION = 138 + +g_axProfile = false + +------------------------------------------------------------------------------- +-- Global variables +------------------------------------------------------------------------------- + +global g_seeThru +global g_skelOnly +global g_updateTime +global g_playAnim +global g_animAtSlider +global g_animTransMode -- 1 = from AnimSet, 2 = force mesh translation, 3 = force AnimSet translation +global g_fixLooping +global g_lastDir1 +global g_lastDir2 +global g_texDir +global g_texRecurse +global g_texMissAction +global g_boneSize +global g_reposBones +global g_rotY +global g_rotP +global g_rotR +global g_transX +global g_transY +global g_transZ +global g_meshScale +global g_reorientBones +global g_dontConjugateRoot +global Anims -- array of AnimInfoBinary + + +------------------------------------------------------------------------------- +-- Default settings +------------------------------------------------------------------------------- + +fn axDefaultSettings = +( + -- defaults settings + g_seeThru = false + g_skelOnly = false + g_updateTime = true + g_playAnim = false + g_animAtSlider = false + g_animTransMode = 1 + g_fixLooping = false + g_lastDir1 = "" + g_lastDir2 = "" + g_texDir = "" + g_texRecurse = true + g_texMissAction = 1 + g_boneSize = 0.5 + g_reposBones = true + g_rotY = 0 + g_rotP = 0 + g_rotR = 0 + g_transX = 0 + g_transY = 0 + g_transZ = 0 + g_meshScale = 1.0 + g_reorientBones = false + g_dontConjugateRoot = false +) + + +------------------------------------------------------------------------------- +-- Configuration +------------------------------------------------------------------------------- + +configFile = undefined +if getSourceFileName != undefined then -- checking Max version (Max9+) ... +( + local s = getSourceFileName() + configFile = (getFilenamePath s) + (getFilenameFile s) + ".ini" +) +else +( + -- workaround for Max 8 and older + configFile = (getDir #scripts) + "\ActorXImporter.ini" +) + + +tmp_v = undefined -- global variable, helper for axDoSetting() (required for execute() ...) +g_isLoading = true -- axDoSetting() mode + +fn axDoSetting name var = +( + local default = execute var -- value has the same type as var + if g_isLoading then + ( + try + ( + -- loading value + tmp_v = getINISetting configFile "Main" name -- get from ini as string + if (tmp_v != "") and (tmp_v != "undefined") then + ( + local type = classOf default +-- format "reading % (%) = %\n" var type tmp_v + if (not isKindOf default String) then + execute (var + "=tmp_v as " + (type as string)) + else + execute (var + "=tmp_v") -- no conversion + ) + ) + catch + ( + format "Reading %: %\n" name (getCurrentException()) + ) + ) + else + ( + -- saving value + setINISetting configFile "Main" name (default as string) + ) +) + + +fn axSerializeSettings isLoading = +( + if configFile == undefined then return undefined -- could happen with old 3ds Max, where getSourceFileName() doesn't exist + if isLoading then + ( + if not doesFileExist configFile then return undefined -- no config file + ) + g_isLoading = isLoading + -- read/write settings + axDoSetting "LastUsedDir" "g_lastDir1" + axDoSetting "LastUsedDir2" "g_lastDir2" + axDoSetting "TexturesDir" "g_texDir" + axDoSetting "TexRecurse" "g_texRecurse" + axDoSetting "TexMissAction" "g_texMissAction" + axDoSetting "AutoPlayAnim" "g_playAnim" + axDoSetting "AnimAtSlider" "g_animAtSlider" + axDoSetting "AnimTransMode" "g_animTransMode" + axDoSetting "UpdateTime" "g_updateTime" + axDoSetting "FixLoopAnim" "g_fixLooping" + axDoSetting "SeeThru" "g_seeThru" + axDoSetting "SkelOnly" "g_skelOnly" + axDoSetting "BoneSize" "g_boneSize" + axDoSetting "ReposBones" "g_reposBones" + axDoSetting "MeshYaw" "g_rotY" + axDoSetting "MeshPitch" "g_rotP" + axDoSetting "MeshRoll" "g_rotR" + axDoSetting "MeshX" "g_transX" + axDoSetting "MeshY" "g_transY" + axDoSetting "MeshZ" "g_transZ" + axDoSetting "MeshScale" "g_meshScale" +-- axDoSetting "ReorientBones" "g_reorientBones" +-- axDoSetting "DontConjRoot" "g_dontConjugateRoot" +) + + +------------------------------------------------------------------------------- +-- Service functions +------------------------------------------------------------------------------- + +fn ErrorMessage text = +( + local msg = ("ERROR: " + text + "\n") + format "%\n" msg + messageBox msg + throw msg +) + + +fn TrimSpaces text = +( + trimLeft(trimRight(text)) +) + + +fn IsEndOfFile bstream = +( + local savePos = ftell bstream + fseek bstream 0 #seek_end -- compute file size + local fileSize = ftell bstream + fseek bstream savePos #seek_set + (savePos >= fileSize) +) + + +fn ReadFixedString bstream fixedLen = +( + local str = "" + local length = 0 + local finished = false + for i = 1 to fixedLen do + ( + local c = ReadByte bstream #unsigned + if c == 0 then finished = true -- end of line char + if not finished then -- has end of line before - skip remaining chars + ( + -- not "finished" string + str += bit.intAsChar(c) -- append a character + if c != 32 then length = i -- position of last non-space char + ) + ) + substring str 1 length -- return first "length" chars +) + +fn ReadVector2 bstream = +( + local v = point2 0 0 + v.x = ReadFloat bstream + v.y = ReadFloat bstream + v +) + +fn ReadFVector bstream = +( + local v = point3 0 0 0 + v.x = ReadFloat bstream + v.y = ReadFloat bstream + v.z = ReadFloat bstream + v +) + +fn ReadFQuat bstream = +( + local q = quat 0 0 0 0 + q.x = ReadFloat bstream + q.y = ReadFloat bstream + q.z = ReadFloat bstream + q.w = ReadFloat bstream + q +) + +-- Function used to determine bone length +fn axFindFirstChild boneArray boneIndex = +( + local res = undefined, notfound = true + for i = 1 to boneArray.count while notfound do + ( + if (i != boneIndex) then + ( + bn = boneArray[i] + if bn.ParentIndex == boneIndex-1 then + ( + res = bn + notfound = false + ) + ) + ) + res +) + + +fn axFixBoneNames boneArray = +( + -- Find and correct duplicate names + for i = 1 to (boneArray.count-1) do + ( + local n = boneArray[i].Name + local dupCount = 1 + for j = (i+1) to boneArray.count do + ( + local n2 = boneArray[j].Name + if n == n2 then + ( + dupCount += 1 + n2 = n + "_" + (dupCount as string) + format "Duplicate bone name \"%\", renamed to \"%\"\n" n n2 + boneArray[j].Name = n2 + ) + ) + ) +) + + +fn axFindFile path filename recurse:false = +( + local res = undefined + local check = path + "\\" + filename + if doesFileExist check then + ( + res = check + ) + else if recurse then + ( + local dirs = getDirectories (path + "/*") + local notfound = true + for dir in dirs while notfound do + ( + res = axFindFile dir filename recurse:true + if res != undefined then + ( + notfound = false -- break the loop + ) + ) + ) + res +) + + +fn axGetRootMatrix = +( + local angles = eulerAngles g_rotR -g_rotP -g_rotY + local m = angles as matrix3 + m.translation = [g_transX, g_transY, g_transZ] + m +) + +-- Reference: https://forums.autodesk.com/t5/3ds-max-programming/getopenfilename-for-multiple-files/td-p/4097903 +fn getMultiOpenFilenames caption: "Open" filename: "" types: "All Files (*.*)|*.*" default: 1 = +( + local dlg = DotNetObject "System.Windows.Forms.OpenFileDialog" + dlg.multiSelect = true + dlg.title = caption + + local p = getFilenamePath filename + if doesFileExist p then + dlg.initialDirectory = p + + -- MAXScript getOpenFilename uses trailing |; + -- OpenFileDialog filter does not. + if types == "|" then + dlg.filter = (substring types 1 (types.count - 1)) + else + dlg.filter = types + + dlg.filterIndex = default + + local result = dlg.ShowDialog() + if (result.Equals result.OK) then + dlg.filenames + else + undefined +) + +g_axProfileBeginTime = 0 + +fn axBeginProfile = +( + g_axProfileBeginTime = timeStamp() +) + +fn axProfilePoint title debug:false = +( + if (not debug or g_axProfile) then + ( + local current = timeStamp() + local delta = current - g_axProfileBeginTime + g_axProfileBeginTime = current + format "% took % s\n" title (delta/1000.0) + ) +) + +------------------------------------------------------------------------------- +-- ActorX data structures +------------------------------------------------------------------------------- + +struct VChunkHeader +( + ChunkID, + TypeFlag, + DataSize, + DataCount +) + +fn ReadChunkHeader bstream = +( + local hdr = VChunkHeader () + hdr.ChunkID = ReadFixedString bstream 20 + hdr.TypeFlag = ReadLong bstream #unsigned + hdr.DataSize = ReadLong bstream #unsigned + hdr.DataCount = ReadLong bstream #unsigned +-- format "Read chunk header: %\n" hdr + hdr +) + +struct VVertex +( + PointIndex, + U, V, + MatIndex, + Reserved, + Pad +) + +fn ReadVVertex bstream = +( + local v = VVertex () + local pad + v.PointIndex = ReadShort bstream #unsigned + pad = ReadShort bstream + v.U = ReadFloat bstream + v.V = ReadFloat bstream + v.MatIndex = ReadByte bstream #unsigned + v.Reserved = ReadByte bstream #unsigned + v.Pad = ReadShort bstream #unsigned + v +) + +fn ReadVVertex32 bstream = +( + local v = VVertex () + v.PointIndex = ReadLong bstream #unsigned -- short -> long, no "pad" + v.U = ReadFloat bstream + v.V = ReadFloat bstream + v.MatIndex = ReadByte bstream #unsigned + v.Reserved = ReadByte bstream #unsigned + v.Pad = ReadShort bstream #unsigned + v +) + +struct VTriangle +( + Wedge0, Wedge1, Wedge2, + MatIndex, + AuxMatIndex, + SmoothingGroups +) + +fn ReadVTriangle bstream = +( + local v = VTriangle () + v.Wedge0 = ReadShort bstream #unsigned + v.Wedge1 = ReadShort bstream #unsigned + v.Wedge2 = ReadShort bstream #unsigned + v.MatIndex = ReadByte bstream #unsigned + v.AuxMatIndex = ReadByte bstream #unsigned + v.SmoothingGroups = ReadLong bstream #unsigned + v +) + +fn ReadVTriangle32 bstream = +( + local v = VTriangle () + v.Wedge0 = ReadLong bstream #unsigned -- short -> long + v.Wedge1 = ReadLong bstream #unsigned -- ... + v.Wedge2 = ReadLong bstream #unsigned -- ... + v.MatIndex = ReadByte bstream #unsigned + v.AuxMatIndex = ReadByte bstream #unsigned + v.SmoothingGroups = ReadLong bstream #unsigned + v +) + +struct VMaterial +( + MaterialName, + TextureIndex, + PolyFlags, + AuxMaterial, + AuxFlags, + LodBias, + LodStyle +) + +fn ReadVMaterial bstream = +( + local m = VMaterial () + m.MaterialName = ReadFixedString bstream 64 + m.TextureIndex = ReadLong bstream #unsigned + m.PolyFlags = ReadLong bstream #unsigned + m.AuxMaterial = ReadLong bstream #unsigned + m.AuxFlags = ReadLong bstream #unsigned + m.LodBias = ReadLong bstream + m.LodStyle = ReadLong bstream + m +) + +struct VColor +( + R, G, B, A +) + +fn ReadVColor bstream = +( + local c = VColor () + c.R = ReadByte bstream #unsigned + c.G = ReadByte bstream #unsigned + c.B = ReadByte bstream #unsigned + c.A = ReadByte bstream #unsigned + c +) + +struct VBone +( + Name, + Flags, + NumChildren, + ParentIndex, + -- VJointPos + Orientation, + Position, + Length, + Size, + -- Computed data + Matrix +) + +fn ReadVBone bstream = +( + local b = VBone () + b.Name = ReadFixedString bstream 64 + b.Flags = ReadLong bstream #unsigned + b.NumChildren = ReadLong bstream + b.ParentIndex = ReadLong bstream + b.Orientation = ReadFQuat bstream + b.Position = ReadFVector bstream + b.Length = ReadFloat bstream + b.Size = ReadFVector bstream + b +) + +struct VRawBoneInfluence +( + Weight, + PointIndex, + BoneIndex +) + +fn ReadVRawBoneInfluence bstream = +( + local v = VRawBoneInfluence () + v.Weight = ReadFloat bstream + v.PointIndex = ReadLong bstream #unsigned + v.BoneIndex = ReadLong bstream #unsigned + v +) + +fn InfluenceSort v1 v2 = +( + -- we just need to get influences sorted by vertex index + local cmp = v1.PointIndex - v2.PointIndex + -- add bone index sorting for sort stability + if (cmp == 0) then cmp = v1.BoneIndex - v2.BoneIndex + cmp +) + +struct AnimInfoBinary +( + Name, + Group, + TotalBones, + RootInclude, + KeyCompressionStyle, + KeyQuotum, + KeyReduction, + TrackTime, + AnimRate, + StartBone, + FirstRawFrame, + NumRawFrames +) + +fn ReadAnimInfoBinary bstream = +( + v = AnimInfoBinary () + v.Name = ReadFixedString bstream 64 + v.Group = ReadFixedString bstream 64 + v.TotalBones = ReadLong bstream + v.RootInclude = ReadLong bstream + v.KeyCompressionStyle = ReadLong bstream + v.KeyQuotum = ReadLong bstream + v.KeyReduction = ReadFloat bstream + v.TrackTime = ReadFloat bstream + v.AnimRate = ReadFloat bstream + v.StartBone = ReadLong bstream + v.FirstRawFrame = ReadLong bstream + v.NumRawFrames = ReadLong bstream + v +) + +struct VQuatAnimKey +( + Position, + Orientation, + Time +) + +fn ReadVQuatAnimKey bstream = +( + local k = VQuatAnimKey () + k.Position = ReadFVector bstream + k.Orientation = ReadFQuat bstream + k.Time = ReadFloat bstream + k +) + + +------------------------------------------------------------------------------- +-- Bone attributes +------------------------------------------------------------------------------- + +AXBoneCustomDataDef = attributes AXBoneCustomData +attribID:#(0xF3DD7FCD, 0x4DB58449) +( + parameters BindPose + ( + AX_RelMatrix type: #matrix3 -- matrix relative to parent bone + AX_WorldMatrix type: #matrix3 -- world matrix + ) +) + +------------------------------------------------------------------------------- +-- Loading materials +------------------------------------------------------------------------------- + +fn axFindTexture texDir baseName = +( + -- DDS + foundTex = axFindFile texDir (baseName + ".dds") recurse:g_texRecurse + if foundTex == undefined then + ( + -- TGA + foundTex = axFindFile texDir (baseName + ".tga") recurse:g_texRecurse + if foundTex == undefined then + ( + -- PNG + foundTex = axFindFile texDir (baseName + ".png") recurse:g_texRecurse + ) + ) + foundTex +) + +fn axImportMaterial matName texDir = +( + local subMat = standardMaterial name:matName + + local texFilename + local foundTex + + -- try to file material file + texFilename = matName + ".mat" + foundTex = axFindFile texDir texFilename recurse:g_texRecurse + if foundTex != undefined then + ( + texFilename = foundTex + format "Loading material %\n" texFilename + local matFile = openFile texFilename + while eof matFile == false do + ( + local line = readline matFile + local tok = filterString line " =" +-- format "[%] = [%]\n" tok[1] tok[2] + local parm = tok[1] + local file = tok[2] + foundTex = axFindTexture texDir file + if foundTex == undefined then continue + local bitmap = bitmapTexture name:foundTex fileName:foundTex + if parm == "Normal" then + ( + local normalMap = normal_bump name:foundTex normal_map:bitmap + subMat.bumpMap = normalMap + subMat.bumpMapAmount = 100 -- amount is set to 30 by default + ) + else + ( + if parm == "Diffuse" then subMat.diffuseMap = bitmap + if parm == "Specular" then subMat.specularMap = bitmap + if parm == "SpecPower" then subMat.specularLevelMap = bitmap + if parm == "Opacity" then subMat.opacityMap = bitmap + if parm == "Emissive" then subMat.selfIllumMap = bitmap + ) + ) + close matFile + return subMat + ) + -- no material file found, try simple texture + -- get texture filename + texFilename = matName + foundTex = axFindTexture texDir matName + if foundTex != undefined then + ( + texFilename = foundTex + ) + else + ( + if g_texMissAction == 2 then -- ask + ( + local check = getOpenFileName caption:("Get texture for material " + matName) \ + types:"Texture files (*.tga,*.dds,*.png)|*.tga;*.dds;*.png|All (*.*)|*.*|" filename:texFilename + if check != undefined then texFilename = check + ) + ) + if not doesFileExist texFilename then format "Unable to find texture %\n" texFilename + -- continue setup (even in a case of error) + local bitmap = bitmapTexture name:texFilename fileName:texFilename + subMat.diffuseMap = bitmap + -- return + subMat +) + +------------------------------------------------------------------------------- +-- MAX helpers +------------------------------------------------------------------------------- + +fn FindAllBones_Recurse bones parent = +( + for i = 1 to parent.children.count do + ( + node = parent.children[i] + if isKindOf node BoneObj then + ( + append bones node + ) + FindAllBones_Recurse bones node + ) +) + +fn FindAllBones = +( + local bones = #() + + FindAllBones_Recurse bones rootNode + + bones +) + +fn RemoveAnimation = +( + stopAnimation() + bones = FindAllBones() + for i = 1 to bones.count do + ( + b = bones[i] + deleteKeys b #allKeys + ) + animationRange = interval 0 1 +) + + +fn RestoreBindpose = +( + RemoveAnimation() + + try + ( + local rotMatrix = axGetRootMatrix() + -- note: should rotate every bone because we are not applying parent's rotation here + -- find bones + bones = FindAllBones() + for i = 1 to bones.count do + ( + b = bones[i] + data = custAttributes.get b AXBoneCustomDataDef + if data != undefined then + ( + b.transform = data.AX_WorldMatrix * rotMatrix + ) +-- else +-- ( +-- format "no info for %\n" b.name +-- ) + ) + + set coordsys world + ) + catch + ( + format "ERROR!\n" + ) +) + + +fn ClearMaxScene = +( + max select all + if $ != undefined then delete $ +) + + +------------------------------------------------------------------------------- +-- Loading PSK file +------------------------------------------------------------------------------- + +fn ImportPskFile filename skelOnly:false = +( + set coordsys world + + local Verts = #() + local Wedges = #() + local Tris = #() + local Materials = #() + local MeshBones = #() + local Infs = #() + local Colors = #() + + --------- Read the file --------- + + local numVerts = 0 + local numWedges = 0 + local numTris = 0 + local numMaterials = 0 + local numBones = 0 + local numInfluences = 0 + local numVertColors = 0 + local numTexCoords = 1 + + local extraUV = #() + + axBeginProfile() + + try + ( + file = fopen filename "rb" + if file == undefined then return undefined + + -- First header -- + hdr = ReadChunkHeader file + if (hdr.ChunkID != "ACTRHEAD") then + ( + ErrorMessage("Bad chunk header: \"" + hdr.ChunkID + "\"") + ) + + while not IsEndOfFile(file) do + ( + hdr = ReadChunkHeader file + local chunkID = hdr.ChunkID + -- check for extra UV set from latest ActorX exporter + -- note: data has the same format as pskx extension, so the same loading code is used + if (chunkID == "EXTRAUVS0") or (chunkID == "EXTRAUVS1") or (chunkID == "EXTRAUVS2") then + chunkID = "EXTRAUV0"; +-- format "Chunk: % (% items, % bytes/item, pos %)\n" hdr.ChunkID hdr.DataCount hdr.DataSize (ftell file) + case chunkID of + ( + -- Points -- + "PNTS0000": + ( + numVerts = hdr.DataCount + Verts[numVerts] = [ 0, 0, 0 ] -- preallocate + for i = 1 to numVerts do Verts[i] = ReadFVector file + ) + + -- Wedges -- + "VTXW0000": + ( + numWedges = hdr.DataCount + Wedges[numWedges] = VVertex () -- preallocate + if numWedges <= 65536 then + ( + for i = 1 to numWedges do Wedges[i] = ReadVVertex file + ) + else + ( + for i = 1 to numWedges do Wedges[i] = ReadVVertex32 file + ) + ) + + -- Faces -- + "FACE0000": + ( + numTris = hdr.DataCount + Tris[numTris] = VTriangle () -- preallocate + for i = 1 to numTris do Tris[i] = ReadVTriangle file + ) + + -- Faces32 -- + "FACE3200": + ( + numTris = hdr.DataCount + Tris[numTris] = VTriangle () -- preallocate + for i = 1 to numTris do Tris[i] = ReadVTriangle32 file + ) + + -- Materials -- + "MATT0000": + ( + numMaterials = hdr.DataCount + if numMaterials > 0 then Materials[numMaterials] = VMaterial () -- preallocate + for i = 1 to numMaterials do Materials[i] = ReadVMaterial file + ) + + -- Bones -- + "REFSKELT": + ( + numBones = hdr.DataCount + if numBones > 0 then MeshBones[numBones] = VBone () -- preallocate + for i = 1 to numBones do + ( + MeshBones[i] = ReadVBone file +-- format "Bone[%] = %\n" (i-1) MeshBones[i].Name + ) + axFixBoneNames MeshBones + ) + + -- Weights -- + "RAWWEIGHTS": + ( + numInfluences = hdr.DataCount + if numInfluences > 0 then Infs[numInfluences] = VRawBoneInfluence () -- preallocate + for i = 1 to numInfluences do Infs[i] = ReadVRawBoneInfluence file + ) + + -- Vertex colors -- + "VERTEXCOLOR": + ( + numVertColors = hdr.DataCount + if numVertColors > 0 then Colors[numVertColors] = VColor () -- preallocate + for i = 1 to numVertColors do Colors[i] = ReadVColor file + ) + + -- Additional UV set -- + "EXTRAUV0": + ( + numUVVerts = hdr.DataCount + if (numUVVerts != numWedges) then ErrorMessage("Bad vertex count for extra UV set") + local UV = #() + UV[numUVVerts] = [ 0, 0 ] + for i = 1 to numUVVerts do UV[i] = ReadVector2 file + extraUV[numTexCoords] = UV + numTexCoords = numTexCoords + 1 + ) + + default: + ( + -- skip unknown chunk + format "Unknown chunk header: \"%\" at %\n" hdr.ChunkID (ftell file) + fseek file (hdr.DataSize * hdr.DataCount) #seek_cur + ) + ) + ) + ) + catch + ( + fclose file + messageBox("Error loading file " + filename) + format "FATAL ERROR: %\n" (getCurrentException()) + return undefined + ) + + format "Read mesh: % verts, % wedges, % tris, % materials, % bones, % influences\n" \ + numVerts numWedges numTris numMaterials numBones numInfluences + fclose file + + axProfilePoint "File loading" + + --------- File is completely read now --------- + + -- generate skeleton + MaxBones = #() + local rotMatrix = matrix3 1 + for i = 1 to numBones do + ( + bn = MeshBones[i] + -- build bone matrix + q = bn.Orientation + if ((i == 1) and not g_dontConjugateRoot) then q = conjugate q + mat = (normalize q) as matrix3 + mat.row4 = bn.Position * g_meshScale + -- transform from parent bone coordinate space to world space + if (i > 1) then + ( + bn.Matrix = mat * MeshBones[bn.ParentIndex + 1].Matrix + ) + else + ( + bn.Matrix = mat + ) + + -- get bone length (just for visual appearance) + childBone = axFindFirstChild MeshBones i + if (childBone != undefined) then + ( + len = (length childBone.Position) * g_meshScale + ) + else + ( + len = 4 -- no children, default length; note: when len = 1 has bugs with these bones! + ) + if len < 4 then len = 4 + -- create Max bone + newBone = getNodeByName bn.Name exact:true ignoreCase:true + if (newBone == undefined) then + ( + if (g_reorientBones == false or childBone == undefined) then + ( + -- create new bone + newBone = bonesys.createbone \ + bn.Matrix.row4 \ + (bn.Matrix.row4 + len * (normalize bn.Matrix.row1)) \ + (normalize bn.Matrix.row3) + ) + else + ( + -- reorient bone matrix to point directly to a child + -- get world position of the child bone + local childPos = childBone.Position * bn.Matrix * g_meshScale + newBone = bonesys.createbone \ + bn.Matrix.row4 \ + childPos \ + bn.Matrix.row3 + ) + newBone.name = bn.Name + newBone.width = g_boneSize + newBone.height = g_boneSize + newBone.setBoneEnable false 0 -- this is a required thing, otherwise a lot of problems would appear + newBone.pos.controller = TCB_position () + newBone.rotation.controller = TCB_rotation () -- required for correct animation + -- setup parent + if (i > 1) then + ( + if (bn.ParentIndex >= i) then + ( + format "Invalid parent % for bone % (%)" bn.ParentIndex (i-1) bn.Name + return undefined + ) + newBone.parent = MaxBones[bn.ParentIndex + 1] + ) + -- store bind pose in custom data block + custAttributes.add newBone AXBoneCustomDataDef + mat = (normalize q) as matrix3 -- rebuild 'mat', but without scale + mat.row4 = bn.Position + newBone.AX_RelMatrix = mat + newBone.AX_WorldMatrix = bn.Matrix + ) + else + ( + -- bone already exists + if g_reposBones then newBone.transform = bn.Matrix + ) + MaxBones[i] = newBone + ) + + -- generate mesh + MaxFaces = #() + MaxVerts = #() + MaxFaces[numTris] = [ 0, 0, 0 ] -- preallocate + MaxVerts[numWedges] = [ 0, 0, 0 ] -- ... + VertList = #(); -- list of wedges linked for each vertex + VertList.count = numVerts -- preallocate + for i = 1 to numVerts do VertList[i] = #() -- initialize with empty array + for i = 1 to numWedges do + ( + local vertId = Wedges[i].PointIndex + 1 + MaxVerts[i] = Verts[vertId] * g_meshScale + append VertList[vertId] i + ) + for i = 1 to numTris do + ( + tri = Tris[i] + w0 = tri.Wedge0 + w1 = tri.Wedge1 + w2 = tri.Wedge2 + MaxFaces[i] = [ w1+1, w0+1, w2+1 ] -- note: reversing vertex order + ) + newMesh = mesh vertices:MaxVerts faces:MaxFaces name:(getFilenameFile filename) + -- texturing + newMesh.xray = g_seeThru + meshop.setNumMaps newMesh (numTexCoords+1) -- 0 is vertex color, 1+ are textures + meshop.setMapSupport newMesh 1 true -- enable texturemap channel + meshop.setNumMapVerts newMesh 1 numWedges -- set number of texture vertices + for i = 1 to numWedges do + ( + -- set texture coordinates + w = Wedges[i] + meshop.setMapVert newMesh 1 i [ w.U, 1-w.V, 1-w.V ] -- V coordinate is flipped + ) + for i = 1 to numTris do + ( + -- setup face vertices and material + tri = Tris[i] + meshop.setMapFace newMesh 1 i [ tri.Wedge1+1, tri.Wedge0+1, tri.Wedge2+1 ] + setFaceMatId newMesh i (tri.MatIndex+1) + setFaceSmoothGroup newMesh i tri.SmoothingGroups + ) + -- extra UV sets (code is similar to above!) + for j = 2 to numTexCoords do + ( + format "Loading UV set #% ...\n" j + uvSet = extraUV[j-1] -- extraUV does not holds 1st UV set + meshop.setMapSupport newMesh j true -- enable texturemap channel + meshop.setNumMapVerts newMesh j numWedges -- set number of texture vertices + for i = 1 to numWedges do + ( + -- set texture coordinates + uv = uvSet[i] + meshop.setMapVert newMesh j i [ uv.x, 1-uv.y, 1-uv.y ] -- V coordinate is flipped + ) + ) + -- vertex colors + if numVertColors > 0 then + ( + format "Loading vertex colors ...\n" + setNumCPVVerts newMesh numVertColors true + defaultVCFaces newMesh + for i = 1 to numVertColors do + ( + c = Colors[i] + setVertColor newMesh i [ c.R, c.G, c.B, c.A ] + ) + ) + + axProfilePoint "Base import" debug:true + + -- import materials + if g_skelOnly then numMaterials = 0 -- do not load materials for this option + -- setup path to materials and textures + local texDir + if g_texDir != "" then + ( + texDir = g_texDir + ) + else + ( + texDir = getFilenamePath filename + ) + -- create materials + newMat = multiMaterial numsubs:numMaterials + for i = 1 to numMaterials do + ( + local subMat = axImportMaterial Materials[i].MaterialName texDir + newMat.materialList[i] = subMat + showTextureMap subMat true +-- format "Material[%] = %\n" i Materials[i].MaterialName + ) + newMesh.material = newMat + + axProfilePoint "Material import" debug:true + + update newMesh + + -- smooth vertex normals accross UV seams + max modify mode + select newMesh + + normalMod = editNormals () + addModifier newMesh normalMod + normalMod.selectBy = 1 + + for i = 1 to VertList.count do + ( + if VertList[i].count > 1 then + ( + local seamWedges = VertList[i] as bitArray + local n = #{} + normalMod.ConvertVertexSelection &seamWedges &n + normalMod.Average selection:n + ) + ) + VertList.count = 0 + collapsestack newMesh + + if numBones <= 0 then + ( + return undefined + ) + + -- code above is common for SkeletalMesh (psk) and StaticMesh (pskx) + -- code below is executed only for SkeletalMesh + + -- generate skin modifier + skinMod = skin() + boneIDMap = #() + + if numBones > 0 then --?? checking not needed as we have numBones always > 0 here + ( + addModifier newMesh skinMod + for i = 1 to numBones do + ( + if i != numBones then + skinOps.addBone skinMod MaxBones[i] 0 + else + skinOps.addBone skinMod MaxBones[i] 1 + ) + -- In Max 2013 the bone IDs are scrambled, so we look them up + -- by bone's name and stores them in a table. + local numSkinBones = skinOps.GetNumberBones skinMod + -- iterate all bones in the Max (could be more than in a mesh) + for i = 1 to numSkinBones do + ( + local boneName = skinOps.GetBoneName skinMod i 0 + -- compare with mesh bones by name + for j = 1 to numBones do + ( + if boneName == MeshBones[j].Name then + ( + boneIDMap[j] = i +-- format "MaxID[%]: %, OriginalID: %\n" i boneName j + j = numBones + 1 -- break the loop (faster than 'exit') + ) + ) + ) + ) + + axProfilePoint "Preparing skin" debug:true + + if skelOnly then + ( + delete newMesh -- non-optimal way, may skip mesh creation + return undefined + ) + +-- redrawViews() + + modPanel.setCurrentObject skinMod -- this operation takes a lot of time, and there's no way to move it or optimize + axProfilePoint "Selecting skin object" debug:true + + -- setup vertex influences (weights) + +/* -- verify if influences are sorted by vertex: sorting takes some time, so try to avoid it + local sorted = true + for i = 2 to numInfluences while sorted do + ( + vert = Infs[i].PointIndex + if (vert < Infs[i-1].PointIndex) then + ( + sorted = false + format "Sorting broken at index % point %\n" i nextPoint + ) + ) + axProfilePoint "Verify sort" debug:true + -- sort if required + if not sorted then */ + ( + qsort Infs InfluenceSort + ) + + axProfilePoint "Sorting influences" debug:true + + -- build vertex to influence map + vertInfStart = #() + vertInfNum = #() + vertInfStart[numVerts] = 0 -- preallocate + vertInfNum[numVerts] = 0 -- ... + count = 0 + for i = 1 to numInfluences do + ( + v = Infs[i] + vert = v.PointIndex+1 + count += 1 + if (i == numInfluences) or (Infs[i+1].PointIndex+1 != vert) then + ( + -- flush + vertInfStart[vert] = i - count + 1 + vertInfNum[vert] = count + count = 0 + ) + ) + + axProfilePoint "Prepare influences" debug:true + +-- progressStart "Setting weights ..." -- shouldn't call progress functions, causes crash in script + disableSceneRedraw() + numRepairedVerts = 0 + numBadVerts = 0 + try + ( + + for wedge = 1 to numWedges do + ( + vert = Wedges[wedge].PointIndex+1 + start = vertInfStart[vert] + numInfs = vertInfNum[vert] + if numInfs == undefined then + ( + numInfs = 0 + format "Vertex % (wedge %) has no weights\n" (vert-1) (wedge-1) + ) + +/* + -- This code uses SetVertexWeights + oldBone = skinOps.GetVertexWeightBoneID skinMod wedge 1 + numWeights = skinOps.GetVertexWeightCount skinMod wedge + if numWeights > 1 then + ( + skinOps.ReplaceVertexWeights skinMod wedge oldBone 1 + ) + for i = 1 to numInfs do + ( + v = Infs[start + i - 1] + b = boneIDMap[v.BoneIndex+1] +-- format "Inf %(%) % : %\n" wedge vert MeshBones[b].Name v.Weight + skinOps.SetVertexWeights skinMod wedge b v.Weight + if b == oldBone then + ( + oldBone = -1 + ) + ) + if oldBone > 0 then + ( + skinOps.SetVertexWeights skinMod wedge oldBone 0 + ) +*/ + -- This code uses ReplaceVertexWeights with arrays, a few times slower; + -- it is still here because of bugs with SetVertexWeights path (SetVertexWeights + -- sometimes adds influences using its own tricky logic) + infBones = #() + infWeights = #() + for i = 1 to numInfs do + ( + v = Infs[start + i - 1] + append infBones boneIDMap[v.BoneIndex + 1] + append infWeights v.Weight + ) + skinOps.ReplaceVertexWeights skinMod wedge infBones infWeights + -- NOTE: older Max versions after ReplaceVertexWeights call performed reset of infBones and + -- infWeights arrays, so we wasn't able to reuse them. At least Max 2015 doesn't do that. + + -- Check is weights were set correctly + numWeights = skinOps.GetVertexWeightCount skinMod wedge + if numWeights != numInfs then + ( + -- We've tried to set weights for this vertex, but MaxScript decided to keep + -- other bones as dependency (bug in ReplaceVertexWeights). Try to repair: + -- enumerate all current weights and set unwanted bone weights to 0 explicitly. + -- Note: it looks like this is not an issue for Max 2014, it appears in 2015: + -- https://trello.com/c/76npwkAY/115-possible-bug-with-importer-on-max-2015 +-- format "Bad vertex: % bones(%) but %\n" wedge numInfs numWeights + for w = 1 to numWeights do + ( + bone = skinOps.GetVertexWeightBoneID skinMod wedge w + found = findItem infBones bone + if found == 0 then + ( + append infBones bone + append infWeights 0 + ) + ) + skinOps.ReplaceVertexWeights skinMod wedge infBones infWeights + numWeights = skinOps.GetVertexWeightCount skinMod wedge + if numWeights != numInfs then + ( +-- format "Bad vertex: %: bones(%) weights(%)\n" wedge infBones infWeights + numBadVerts += 1 + ) + else + ( + numRepairedVerts += 1 + ) + ) +-- progressUpdate (100.0 * wedge / numWedges) + ) + ) + catch + ( + enableSceneRedraw() +-- progressEnd() + throw() + ) + enableSceneRedraw() + + axProfilePoint "Import influences" debug:true + +-- progressEnd() + if (numRepairedVerts > 0) or (numBadVerts > 0) then + ( + format "Problems during skinning: % bad vertices, % repaired vertices\n" numBadVerts numRepairedVerts + ) + + -- apply mesh rotation + if numBones >= 1 then + ( + MaxBones[1].transform = MaxBones[1].transform * axGetRootMatrix() + ) + + axProfilePoint "Mesh import" + + gc() +) + + +------------------------------------------------------------------------------- +-- Loading PSA file +------------------------------------------------------------------------------- + +fn FindPsaTrackIndex Anims Name = +( + local notfound = true, res = -1 + for i = 1 to Anims.count while notfound do + ( + if Anims[i].Name == Name then + ( + res = i + notfound = false + ) + ) + res +) + +fn FindPsaBoneIndex Bones Name = +( + local notfound = true, res = -1 + for i = 1 to Bones.count while notfound do + ( + if Bones[i].Name == Name then + ( + res = i + notfound = false + ) + ) + res +) + +-- UseAnimTranslation[] is array of flags signalling that particular bone should use translation +-- from the animation; when value is set to false, mesh translation will be used +fn LoadPsaConfig filename Anims Bones UseAnimTranslation AnimFlags = +( + -- allocate and initialize UseAnimTranslation array + UseAnimTranslation[Bones.count] = true -- preallocate + for i = 1 to Bones.count do UseAnimTranslation[i] = true + -- root bone is always translated, start with index 2 below + + case g_animTransMode of + ( +-- 1: - use from AnimSet, do nothing here + 2: ( + for i = 2 to Bones.count do UseAnimTranslation[i] = false + return undefined + ) + 3: ( + for i = 2 to Bones.count do UseAnimTranslation[i] = true -- old behaviour - everything will be taken from the animation + return undefined + ) + ) + + -- read configuration file + local cfgFile = openFile filename + if cfgFile == undefined then return undefined + + local mode = 0 + + while eof cfgFile == false do + ( + local line = readline cfgFile + + -- process directove + case line of + ( + "": continue -- empty line + "[AnimSet]": ( mode = 1; continue ) + "[UseTranslationBoneNames]": ( mode = 2; continue ) + "[ForceMeshTranslationBoneNames]": ( mode = 3; continue ) + "[RemoveTracks]": + ( + mode = 4 + -- allocate AnimFlags array, usually not required (currently used for UC2 animations only) + local numKeys = Anims.count * Bones.count + AnimFlags[numKeys] = 0 -- preallocate + for i = 1 to numKeys do AnimFlags[i] = 0 + continue + ) + ) + + -- process ordinary line + case mode of + ( + 0: ErrorMessage("unexpected \"" + line + "\"") + + -- AnimSet + 1: ( + --!! ugly parsing ... but no other params yet + if line == "bAnimRotationOnly=1" then + ( + for i = 2 to Bones.count do UseAnimTranslation[i] = false + ) + else if line == "bAnimRotationOnly=0" then + ( + -- already set to true + ) + else + ( + ErrorMessage("unexpected AnimSet instruction \"" + line + "\"") + ) + ) + + -- UseTranslationBoneNames - use translation from animation, useful with bAnimRotationOnly=true only + 2: ( + local BoneIndex = FindPsaBoneIndex Bones line + if BoneIndex > 0 then + ( + UseAnimTranslation[BoneIndex] = true + ) + else + ( + format "WARNING: UseTranslationBoneNames has specified unknown bone \"%\"\n" line + ) + ) + + -- ForceMeshTranslationBoneNames - use translation from mesh + 3: ( + local BoneIndex = FindPsaBoneIndex Bones line + if BoneIndex > 0 then + ( + UseAnimTranslation[BoneIndex] = false + ) + else + ( + format "WARNING: ForceMeshTranslationBoneNames has specified unknown bone \"%\"\n" line + ) + ) + + -- RemoveTracks + 4: ( + -- line is in format "SequenceName.BoneIndex=[trans|rot|all]" + local tok1 = filterString line "=" -- [1] = SequenceName.BoneIndex, [2] = Flags + local tok2 = filterString tok1[1] "." -- [1] = SequenceName, [2] = BoneIndex + local SeqName = TrimSpaces(tok2[1]) + local BoneIdxStr = TrimSpaces(tok2[2]) + local Flag = TrimSpaces(tok1[2]) + local SeqIdx = FindPsaTrackIndex Anims SeqName --?? can cache this value + if SeqIdx <= 0 then ErrorMessage("Animation \"" + SeqName + "\" does not exists" + "\nline:" + line) + FlagIndex = (SeqIdx - 1) * Bones.count + (BoneIdxStr as integer) + 1 + if Flag == "trans" then + ( + AnimFlags[FlagIndex] = 1 -- NO_TRANSLATION + ) + else if Flag == "rot" then + ( + AnimFlags[FlagIndex] = 2 -- NO_ROTATION + ) + else if Flag == "all" then + ( + AnimFlags[FlagIndex] = 3 -- NO_TRANSLATION | NO_ROTATION + ) + else + ( + ErrorMessage("unknown RemoveTracks flag \"" + Flag + "\"") + ) + ) + + default: + ErrorMessage("unexpected config error") + ) + ) + close cfgFile +) + + +fn ImportPsaFile filename trackNum all:false = +( + local Bones = #() + Anims = #() + + local UseAnimTranslation = #() + local AnimFlags = #() + + local numBones = 0 + local numAnims = 0 + + local keyPos = 0 + + --------- Read the file --------- + + try + ( + file = fopen filename "rb" + if file == undefined then return undefined + + -- First header -- + hdr = ReadChunkHeader file + if (hdr.ChunkID != "ANIMHEAD") then + ( + ErrorMessage("Bad chunk header: \"" + hdr.ChunkID + "\"") + ) + + while not IsEndOfFile(file) do + ( + hdr = ReadChunkHeader file +-- format "Chunk: % (% items, % bytes/item, pos %)\n" hdr.ChunkID hdr.DataCount hdr.DataSize (ftell file) + case hdr.ChunkID of + ( + -- Bone links -- + "BONENAMES": + ( + numBones = hdr.DataCount + if numBones > 0 then Bones[numBones] = VBone () -- preallocate + for i = 1 to numBones do Bones[i] = ReadVBone file + ) + + -- Animation sequence info -- + "ANIMINFO": + ( + numAnims = hdr.DataCount + if numAnims > 0 then Anims[numAnims] = AnimInfoBinary () -- preallocate + for i = 1 to numAnims do Anims[i] = ReadAnimInfoBinary file + + if trackNum < 0 then + ( + -- information only + fclose file + return undefined + ) + ) + + -- Key data -- + "ANIMKEYS": + ( + -- determine chunk of the file to load later + if all then trackNum = 1 + keyPos = ftell file + for i = 1 to trackNum - 1 do + keyPos += Anims[i].NumRawFrames * numBones * 32 + if all then + numFrames = hdr.DataCount / Bones.count + else + numFrames = Anims[trackNum].NumRawFrames + -- skip this chunk + fseek file (hdr.DataSize * hdr.DataCount) #seek_cur + ) + + default: + ( + -- skip unknown chunk + format "Unknown chunk header: \"%\" at %\n" hdr.ChunkID (ftell file) + fseek file (hdr.DataSize * hdr.DataCount) #seek_cur + ) + ) + ) + ) + catch + ( + fclose file + messageBox ("Error loading file " + filename) + format "FATAL ERROR: %\n" (getCurrentException()) + throw() + return undefined + ) + + if numBones < 1 then + ( + format "Animations has no bones\n" + return undefined + ) + + if keyPos == 0 then + ( + format "No ANIMKEYS chunk was found\n" + return undefined + ) + + -- find existing scene bones + MaxBones = #() + BindPoseInfo = #() + SceneBones = FindAllBones() + for i = 1 to numBones do + ( + boneName = Bones[i].Name + local notfound = true + for j = 1 to SceneBones.count while notfound do + ( + b = SceneBones[j] + if (stricmp b.name boneName) == 0 then + ( + MaxBones[i] = b + BindPoseInfo[i] = custAttributes.get b AXBoneCustomDataDef -- could be 'undefined' + notfound = false + ) + ) + + if notfound then + ( + format "WARNING: cannot find bone %\n" boneName + ) + else if BindPoseInfo[i] == undefined then + ( + format "WARNING: cannot get bind pose information for bone %\n" boneName + ) + ) + + -- verify for found root bone + if MaxBones[1] == undefined then + ( + messageBox ("WARNING: Unable to find root bone \"" + Bones[1].Name + "\"\nAnimation may appear incorrectly!") + ) + + set coordsys world + startframe = 0 -- can modify layer ... + if g_animAtSlider then + ( + startframe = sliderTime + ) + else + ( + RemoveAnimation() + ) + + LoadPsaConfig ( (getFilenamePath filename) + (getFilenameFile filename) + ".config" ) Anims Bones UseAnimTranslation AnimFlags + +/* + format "[% trans % flags]\n" UseAnimTranslation.count AnimFlags.count + for i = 1 to UseAnimTranslation.count do + ( + if UseAnimTranslation[i] then format "trans: % %\n" i Bones[i].Name + ) +*/ + + format "Loading track % (%), % keys\n" trackNum Anims[trackNum].Name (numFrames * Bones.count) + firstFrame = #() + firstFlag = (trackNum - 1) * numBones + 1 + flagCount = AnimFlags.count + fseek file keyPos #seek_set -- seek to animation keys + + animate on + ( + progressStart "Loading animation ..." + for i = 1 to numFrames do + ( + at time (startframe + i - 1) + ( + flagIndex = firstFlag + for b = 1 to Bones.count do + ( + -- get key + k = ReadVQuatAnimKey file -- read key from file + -- get bones + bone = MaxBones[b] -- scene bone to transform + BindPose = BindPoseInfo[b] -- for BindPose transform + -- get animation flags + flag = 0 + if flagIndex < flagCount then flag = AnimFlags[flagIndex] + flagIndex = flagIndex + 1 + -- when either scene or mesh bone is missing, skip everything (key was already read) + if bone == undefined then continue + + local mat + if BindPose != undefined then + ( + -- rotation + if (bit.and flag 2) != 0 then -- NO_ROTATION + ( + -- rotation from mesh + mat = BindPose.AX_RelMatrix + ) + else + ( + -- rotation from animation + q = k.Orientation + if ((b == 1) and not g_dontConjugateRoot) then q = conjugate q + mat = (q as matrix3) + ) + -- translation + if (bit.and flag 1) != 0 then -- NO_TRANSLATION + ( + -- translation from the mesh + mat.row4 = BindPose.AX_RelMatrix.row4 * g_meshScale + ) + else if not UseAnimTranslation[b] then + ( + -- translation from the mesh + mat.row4 = BindPose.AX_RelMatrix.row4 * g_meshScale + ) + else + ( + -- translation from animation + mat.row4 = k.Position * g_meshScale + ) + ) + else + ( + -- the BindPose object doesn't exists, use all data from the animation + q = k.Orientation -- rotation from animation + p = k.Position * g_meshScale -- translation from animation + -- build matrix + if ((b == 1) and not g_dontConjugateRoot) then q = conjugate q + -- build matrix + mat = (q as matrix3) + mat.row4 = p + ) + + -- modify bone + if bone.parent != undefined then + ( + bone.transform = mat * bone.parent.transform + ) + else + ( + bone.transform = mat + ) + -- remember 1st frame + if (i == 1) then firstFrame[b] = bone.transform + ) + -- rotate animation + if MaxBones[1] != undefined then + ( + MaxBones[1].transform = MaxBones[1].transform * axGetRootMatrix() + ) + ) + -- progress bar + progressUpdate (100.0 * i / numFrames) + if getProgressCancel() then exit + ) + if g_fixLooping then + ( + -- Add extra 2 frames for correct TCB controller work. + -- The second frame is not necessary if there is no keys after last frame + -- (may purge all keys before animation loading instead of adding 2nd key) + for i = 0 to 1 do + ( + at time (startframe + numFrames + i) + for b = 1 to Bones.count do + ( + bone = MaxBones[b] + if bone != undefined then + ( + bone.transform = firstFrame[b] + ) + ) + ) + ) + progressEnd() + ) + + -- finish loading + fclose file + + sliderTime = 1 + extraFrame = 0 + if g_fixLooping then extraFrame = 1 + + if g_updateTime then + ( + ar_start = startframe + ar_end = startframe + numFrames - 1 + extraFrame + ) + else + ( + ar_start = animationRange.start.frame + ar_end = animationRange.end.frame + if animationRange.start.frame > startframe then + ar_start = startframe + if animationRange.end.frame < startframe + numFrames + extraFrame then + ar_end = startframe + numFrames - 1 + extraFrame + ) + if (ar_end == ar_start) then ar_end = ar_end + 1 -- avoid zero-length intervals + + animationRange = interval ar_start ar_end + sliderTime = startframe +-- frameRate = track.AnimRate + + if g_playAnim then playAnimation immediateReturn:true + + gc() +) + + +------------------------------------------------------------------------------- +-- User interface +------------------------------------------------------------------------------- + +-- layout +global axRolloutList +global axRolloutStates +global g_axScrollPos + +fn axStoreLayout roll = +( + if axRolloutStates == undefined then axRolloutStates = #() + for i = 1 to axRolloutList.count do + ( + axRolloutStates[i] = axRolloutList[i].open + ) + -- sometimes 'roll' is non-null, but it's property 'scrollPos' is inaccessible + if roll.scrollPos != undefined then g_axScrollPos = roll.scrollPos +) + + +fn axRestoreLayout roll = +( + if axRolloutStates != undefined then + ( + for i = 1 to axRolloutList.count do + ( + axRolloutList[i].open = axRolloutStates[i] + ) + ) + -- when execing first time, layout will not be stored, and g_axScrollPos will be undefined + if g_axScrollPos != undefined then roll.scrollPos = g_axScrollPos +) + + +global MeshFileName +global AnimFileName + +fn axLoadAnimation index = +( + if (index > 0) and (index <= Anims.count) then ImportPsaFile AnimFileName index +) + + +rollout axInfoRollout "ActorX Importer" +( + -- copyright label + label Lbl1 "Version 1.38" + label Lbl2 "\xA9 2009-2020 Konstantin Nosov (Gildor)" + hyperlink Lbl3 "http://www.gildor.org/" \ + address:"http://www.gildor.org/projects/unactorx" align:#center \ + color:black hovercolor:blue visitedcolor:black + + on axInfoRollout close do + ( + format "Saving settings ...\n" + axSerializeSettings false + axStoreLayout axInfoRollout + ) +) + + +rollout axMeshImportRollout "Mesh Import" +( + checkbox ChkSeeThru "See-Thru Mesh" checked:g_seeThru + checkbox ChkSkelOnly "Load skeleton only" checked:g_skelOnly + button BtnImportPsk "Import PSK ..." + + -- event handlers + + on ChkSeeThru changed state do g_seeThru = state + on ChkSkelOnly changed state do g_skelOnly = state + + on BtnImportPsk pressed do + ( + if DotNetObject == undefined then + ( + -- older Max didn't have functionality for getMultiOpenFilenames + local filename = getOpenFileName types:"ActorX Mesh (*.psk,*.pskx)|*.psk;*.pskx|All (*.*)|*.*|" filename:g_lastDir1 + if filename != undefined then + ( + MeshFileName = filename + g_lastDir1 = getFilenamePath MeshFileName + if DoesFileExist MeshFileName then ImportPskFile MeshFileName skelOnly:g_skelOnly + ) + ) + else + ( + local filenames = getMultiOpenFilenames types:"ActorX Mesh (*.psk,*.pskx)|*.psk;*.pskx|All (*.*)|*.*" filename:g_lastDir1 + if filenames != undefined then + ( + for filename in filenames do + ( + MeshFileName = filename + g_lastDir1 = getFilenamePath MeshFileName + if DoesFileExist MeshFileName then ImportPskFile MeshFileName skelOnly:g_skelOnly + ) + ) + ) + ) +) + + +rollout axAnimImportRollout "Animation Import" +( + Group "Animation Import" + ( + button BtnImportPsa "Import PSA ..." + listbox LstAnims "Animations:" height:13 + checkbox ChkAnimTime "Update animation length" checked:g_updateTime + checkbox ChkFixLooping "Fix loop animation" checked:g_fixLooping tooltip:"Append 1st keyframe to animation\ntrack for smooth loop" + checkbox ChkPlayAnim "Play animation" checked:g_playAnim + checkbox ChkAtSlider "Import at slider position" checked:g_animAtSlider + dropdownlist LstTransMode "Translation mode" items:#("Use from AnimSet", "Force mesh translation", "Force AnimSet translation") selection:g_animTransMode + button BtnImportTrk "Load track" across:2 + button BtnImportAll "Load all" tooltip:"Load all animations as a single track" + ) + + -- event handlers + + on BtnImportPsa pressed do + ( + local filename = getOpenFileName types:"ActorX Animation (*.psa)|*.psa|All (*.*)|*.*|" filename:g_lastDir2 + + if filename != undefined then + ( + AnimFileName = filename + g_lastDir2 = getFilenamePath AnimFileName + if DoesFileExist AnimFileName then + ( + ImportPsaFile AnimFileName -1 + LstAnims.items = for a in Anims collect (a.Name + " [" + (a.NumRawFrames as string) + "]") + ) + ) + ) + + on BtnImportTrk pressed do axLoadAnimation LstAnims.selection + on BtnImportAll pressed do ImportPsaFile AnimFileName 1 all:true + on LstAnims doubleClicked sel do axLoadAnimation sel + + on ChkAnimTime changed state do g_updateTime = state + on ChkFixLooping changed state do g_fixLooping = state + on ChkPlayAnim changed state do g_playAnim = state + on ChkAtSlider changed state do g_animAtSlider = state + on LstTransMode selected mode do g_animTransMode = mode + + on axAnimImportRollout open do + ( + -- fill LstAnims + LstAnims.items = for a in Anims collect (a.Name + " [" + (a.NumRawFrames as string) + "]") + ) +) + + +rollout axTexturesRollout "Materials" +( + edittext EdTexPath "Path to materials" text:g_texDir width:180 across:2 + button BtnBrowseTex "..." align:#right height:16 + checkbox ChkTexRecurse "Look in subfolders" checked:g_texRecurse + label LblMissingTex "On missing texture:" across:2 + radiobuttons RadMissingTex labels:#("do nothing", "ask") default:g_texMissAction align:#left columns:1 + + on EdTexPath changed val do g_texDir = val + on BtnBrowseTex pressed do + ( + dir = getSavePath caption:"Directory for texture lookup" initialDir:g_texDir + if dir != undefined then + ( + g_texDir = dir + EdTexPath.text = dir + ) + ) + + on ChkTexRecurse changed state do g_texRecurse = state + on RadMissingTex changed state do g_texMissAction = state +) + + +rollout axToolsRollout "Tools" +( + button BtnReset "Reset to defaults" width:180 + + button BtnRestoreBindpose "Restore BindPose" width:180 + button BtnRemoveAnimation "Remove animation" width:180 + button BtnClearScene "Clear scene" width:180 + button BtnBatchExport "Batch export" width:180 + button BtnReloadScript "Reload importer" width:180 + + on BtnReset pressed do + ( + if configFile != undefined then deleteFile configFile + axDefaultSettings() + -- reset controls + axShowUI() + ) + on BtnRestoreBindpose pressed do RestoreBindpose() + on BtnRemoveAnimation pressed do RemoveAnimation() + on BtnClearScene pressed do ClearMaxScene() + on BtnBatchExport pressed do fileIn "export_fbx.ms" + on BtnReloadScript pressed do + ( + if getSourceFileName != undefined then -- checking Max version (Max9+) ... + ( + axStoreLayout axInfoRollout + fileIn(getSourceFileName()) + ) + ) +) + + +rollout axSettingsRollout "Mesh Settings" +( + spinner SpnBoneSize "Bone size" range:[0.1,10,g_boneSize] type:#float scale:0.1 align:#left across:2 + spinner SpnMeshScale "Mesh scale" range:[0.01,1000,g_meshScale] type:#float scale:0.01 align:#right + checkbox ChkRepBones "Reposition existing bones" checked:g_reposBones + + group "Mesh rotation" + ( + spinner SpnRY "Yaw" range:[-180,180,g_rotY] type:#integer scale:90 fieldwidth:35 align:#left across:3 + spinner SpnRP "Pitch" range:[-180,180,g_rotP] type:#integer scale:90 fieldwidth:35 + spinner SpnRR "Roll" range:[-180,180,g_rotR] type:#integer scale:90 fieldwidth:35 align:#right + button BtnRotMaya "Maya" across:3 + button BtnRotReset "Reset" + button BtnRotApply "Apply" + ) + + group "Mesh offset" + ( + spinner SpnTX "X" range:[-10000,10000,g_transX] type:#float scale:0.01 fieldwidth:50 align:#left across:3 + spinner SpnTY "Y" range:[-10000,10000,g_transY] type:#float scale:0.01 fieldwidth:50 + spinner SpnTZ "Z" range:[-10000,10000,g_transZ] type:#float scale:0.01 fieldwidth:50 align:#right + ) + + -- event handlers + on SpnBoneSize changed val do g_boneSize = val + on SpnMeshScale changed val do g_meshScale = val + on ChkRepBones changed state do g_reposBones = state + + on SpnRY changed val do g_rotY = val + on SpnRP changed val do g_rotP = val + on SpnRR changed val do g_rotR = val + on SpnTX changed val do g_transX = val + on SpnTY changed val do g_transY = val + on SpnTZ changed val do g_transZ = val + + on BtnRotMaya pressed do + ( + g_rotY = SpnRY.value = -90 + g_rotP = SpnRP.value = 0 + g_rotR = SpnRR.value = 90 + RestoreBindpose() + ) + on BtnRotReset pressed do + ( + g_rotY = SpnRY.value = 0 + g_rotP = SpnRP.value = 0 + g_rotR = SpnRR.value = 0 + RestoreBindpose() + ) + on BtnRotApply pressed do RestoreBindpose() +) + + +rollout axAdvSettingsRollout "Advanced Settings" +( + label Lbl1 "WARNING: do not modify these settings" + label Lbl2 "unless you know what you are doing!" + checkbox ChkReorientBones "Reorient bones" checked:g_reorientBones + checkbox ChkDontConjRoot "Don't conjugate root bone" checked:g_dontConjugateRoot + + -- event handlers + on ChkReorientBones changed state do g_reorientBones = state + on ChkDontConjRoot changed state do g_dontConjugateRoot = state +) + + +global axImportFloater + +fn axShowUI = +( + -- request position of previous window, if it was already opened + local x = 30 + local y = 100 + local w = 250 + local h = 700 + if axImportFloater != undefined then + ( + x = axImportFloater.pos.x + y = axImportFloater.pos.y + w = axImportFloater.size.x + h = axImportFloater.size.y + -- close old window + closeRolloutFloater axImportFloater + ) + -- Create plugin window + axImportFloater = newRolloutFloater "ActorX Import" w h x y -- create a new window + + -- init axRolloutList + axRolloutList = #(axInfoRollout, axMeshImportRollout, axAnimImportRollout, axTexturesRollout, axToolsRollout, axSettingsRollout, axAdvSettingsRollout) + + -- add controls + for i = 1 to axRolloutList.count do + ( + addRollout axRolloutList[i] axImportFloater + ) + + axRestoreLayout axInfoRollout +) + + +------------------------------------------------------------------------------- +-- Plugin startup +------------------------------------------------------------------------------- + +global g_axImporterVersion + +if (g_axImporterVersion == undefined) then +( + -- initialize plugin + heapSize += 33554432 -- 32 Mb; will speedup most tasks + Anims = #() + g_axImporterVersion = AX_IMPORTER_VERSION + axDefaultSettings() + axSerializeSettings(true) + + if getSourceFileName != undefined then -- checking Max version (Max9+) ... + ( + -- Add action handler (macro script). + -- Max will copy contents of macroScript() block to the "AppData/Local/Autodesk/3dsMax/*/ENU/usermacros". + -- To avoid copying of entire file we're generating string which will simply execute THIS file. + str = "macroScript GildorTools_ActorXImporter category:\"Gildor Tools\" buttontext:\"ActorX Importer\" tooltip:\"ActorX Importer\"\n" \ + + "(\n" \ + + " fileIn \"" + getSourceFileName() + "\"\n" \ + + ")\n" + execute str + ) +) + + +if (g_axImporterVersion != AX_IMPORTER_VERSION) then +( + format "ActorX Importer has been updated while 3ds Max is running.\nReloading settings.\n" + -- copy-paste of code above + g_axImporterVersion = AX_IMPORTER_VERSION + axDefaultSettings() + axSerializeSettings(true) +) + +axShowUI() + + + + + +------------------------------------------------------------------------------------------------------- + + -- START OF EXPORT CODE + +------------------------------------------------------------------------------------------------------- + + + + + +/* + + ActorX batch converter for 3ds Max + + Created: December 15 2010 + + Author: Konstantin Nosov (aka Gildor) + + Web page: http://www.gildor.org/projects/unactorx + + Revision History: + + 06.12.2017 v1.07 + - renamed "recurse" option to "look in subfolders" to be less confising to new users + + 20.09.2017 v1.06 + - fixed error in fbx animation export when track has 0 frames + + 17.01.2017 v1.05 + - fixed non-multitake animation export + - fixed length of exported animation + + 31.07.2016 v1.04 + - option to save multiple animation is single FBX file (multi-take animation) + + 25.07.2016 v1.03 + - saving animation name as FBX Take name (thanks Skykila) + + 21.07.2015 v1.02 + - updated to match ActorX Importer 1.33 changes, improved appearance + + 29.12.2010 v1.01 + - added output format selection (fbx, ase, max) + + 15.12.2010 v1.00 + - first public release + +*/ + + +/* TODO + - save settings to the ActorX Importer ini (using its API) +*/ + +global g_axImporterVersion + +global g_meshDir +global g_meshRecurse +global g_fbxSmGroups +global g_fbxSmMesh +global g_fbxMultiTake +global g_outFormat +global g_useDefaultFBXSettings + +if (g_meshDir == undefined) then g_meshDir = "" +if (g_meshRecurse == undefined) then g_meshRecurse = false +if (g_fbxSmGroups == undefined) then g_fbxSmGroups = true +if (g_fbxSmMesh == undefined) then g_fbxSmMesh = true +if (g_fbxMultiTake == undefined) then g_fbxMultiTake = false +if (g_outFormat == undefined) then g_outFormat = 1 +if (g_useDefaultFBXSettings == undefined) then g_useDefaultFBXSettings = true + + +fn VerifyAXI = +( + if (g_axImporterVersion == undefined) then + ( + messageBox "ActorX Importer is not loaded!" + return false + ) + if (g_axImporterVersion < 133) then + ( + messageBox "Your ActorX Importer script is too old, please update!" + return false + ) + return true +) + + +-- configure FBX exporter +fn SetupFBX = +( + if g_useDefaultFBXSettings then return undefined + + -- http://www.the-area.com/forum/autodesk-fbx/fbx-plug-ins-import-export-discussions/maxscript-export-dialog-properties/ + -- both commands should be used to ensure all commands are functional + pluginManager.loadClass FBXIMP + pluginManager.loadClass FBXEXP + + -- FbxExporterSetParam "Geometries" true -- + -- Controls the state of the "Geometries" checkbox in the FBX Export dialog. + FbxExporterSetParam "NormalsPerPoly" true -- + -- Controls the state of the "Support normals per polygon vertex" checkbox in the FBX Export dialog. + FbxExporterSetParam "Cameras" false -- + -- Controls the state of the "Cameras" checkbox in the FBX Export dialog. + FbxExporterSetParam "Lights" false -- + -- Controls the state of the "Lights" checkbox in the FBX Export dialog. + FbxExporterSetParam "GeomAsBone" true -- + -- Controls the state of the "Geometries used as bones, exported as bones" checkbox in the FBX Export dialog. + FbxExporterSetParam "Shape" false -- + -- Controls the state of the "Shape (Morph modifier)" checkbox in the FBX Export dialog. + FbxExporterSetParam "Skin" true -- + -- Controls the state of the "Skins (Skin Modifier and Physique)" checkbox in the FBX Export dialog. + FbxExporterSetParam "Animation" true -- + -- Controls the state of the "Animation" checkbox in the FBX Export dialog. + -- FbxExporterSetParam "Resampling" -- + -- Controls the value of the "Resampling rate (when necessary)" field in the FBX Export dialog. + FbxExporterSetParam "ShowWarnings" false -- + -- Controls the state of the "Show warnings" checkbox in the FBX Export dialog. + FbxExporterSetParam "EmbedTextures" false -- + -- Controls the state of the "Embed textures in export file" checkbox in the FBX Export dialog. + FbxExporterSetParam "SmoothingGroups" g_fbxSmGroups -- + -- True or false. See Smoothing Groups for an explanation of this setting. + FbxExporterSetParam "SmoothMeshExport" g_fbxSmMesh -- + -- True or false. See TurboSmooth for an explanation of this setting. +) + + +fn GetExportSubDir = +( + if (g_outFormat == 1) then + ( + return "FBX" + ) + if (g_outFormat == 2) then + ( + return "ase" + ) + if (g_outFormat == 3) then + ( + return "max" + ) + return "unknown" -- should not get here +) + + +fn SaveAXFile filename = +( + if (g_outFormat == 1) then + ( + -- FBX + exportFile filename #noPrompt using:FBXEXP + return undefined + ) + if (g_outFormat == 2) then + ( + -- ASE + exportFile (filename + ".ase") #noPrompt + return undefined + ) + if (g_outFormat == 3) then + ( + -- MAX + saveMaxFile filename + return undefined + ) +) + + +fn ExportFbxAnim givenPath:"" = +( + if (not VerifyAXI()) then return undefined + + bones = FindAllBones() + if (bones.count == 0) then + ( + messageBox "Mesh is not loaded!" + return undefined + ) + + if (Anims.count == 0) then + ( + messageBox "AnimSet is not loaded!" + return undefined + ) + + SetupFBX() + + -- configure ActorX Importer + local playAnim = g_playAnim -- save + g_playAnim = false + + -- export all animations + if (g_fbxMultiTake == false) then + ( + -- create target directory + local dir = getFilenamePath(AnimFileName) + GetExportSubDir() + "\\" + if givenPath != "" do + dir = givenPath + "/FBX/" + + makeDir dir all:true + + for i = 1 to Anims.count do + ( + local track = Anims[i] + local trackName = track.Name + local filename = dir + trackName + local numFrames = track.NumRawFrames-1 + if (numFrames == 0) then numFrames = 1 + -- clear Take information before the export + FBXExporterSetParam "SplitAnimationIntoTakes" "-clear" + FBXExporterSetParam "SplitAnimationIntoTakes" trackName 0 numFrames + format "Exporting animation % (% frames) -> %\n" trackName numFrames filename + + ImportPsaFile AnimFileName i + SaveAXFile filename + ) + ) + else + ( + local dir = getFilenamePath(AnimFileName) + GetExportSubDir() + makeDir dir all:true + local filename = dir + "\\" + getFilenameFile(AnimFileName) + format "Exporting all animations -> %\n" filename + ImportPsaFile AnimFileName 1 all:true + for i = 1 to Anims.count do + ( + local track = Anims[i] + local trackName = track.Name + local numFrames = track.NumRawFrames-1 + if (numFrames < 1) then numFrames = 1 + FBXExporterSetParam "SplitAnimationIntoTakes" trackName track.FirstRawFrame (track.FirstRawFrame+numFrames) + ) + SaveAXFile filename + ) + + -- clear Take information after the export + FBXExporterSetParam "SplitAnimationIntoTakes" "-clear" + + g_playAnim = playAnim -- restore +) + + +fn ExportFbxMesh psk_filename = +( + if (not VerifyAXI()) then return undefined + SetupFBX() + +-- format "MESH: %\n" filename + + -- create target directory + local dir = (getFilenamePath psk_filename) + makeDir dir all:true + local filename = dir + "\\" + getFilenameFile(psk_filename) + format "Exporting mesh % -> %\n" psk_filename filename + + ClearMaxScene() + ImportPskFile psk_filename + SaveAXFile filename +) + +fn ExportFbxMeshes path recurse = +( + if (not VerifyAXI()) then return undefined +-- format "EXPORT DIR % %\n" path recurse + + local files = getFiles(path + "/*.psk*") + for file in files do ExportFbxMesh file + if recurse then + ( + local dirs = getDirectories(path + "/*") + for dir in dirs do ExportFbxMeshes dir recurse + ) + + ClearMaxScene() +) + + +-- UI +fn CreateExporterWindow = () +global fbxExportFloater + +rollout fbxExportRollout "ActorX Batch Export" +( + -- copyright label + label Lbl1 "Version 1.07" + label Lbl2 "\xA9 2010-2020 Konstantin Nosov (Gildor)" + hyperlink Lbl3 "http://www.gildor.org/" \ + address:"http://www.gildor.org/projects/unactorx" align:#center \ + color:black hovercolor:blue visitedcolor:black + + group "Common" + ( + label LblOutFormat "Output format:" across:2 + radiobuttons RadOutFormat labels:#("fbx", "ase", "max") default:g_outFormat align:#left columns:1 + checkbox ChkDefFbxSettings "Use default FBX settings" checked:g_useDefaultFBXSettings + ) + + group "Meshes" + ( + label Lbl10 "This tool will convert all PSK meshes from" align:#left + label Lbl11 "selected directory to specified output format" align:#left + label Lbl12 "" + edittext EdMeshPath "Path to PSK" text:g_meshDir width:180 across:2 + button BtnBrowseMesh "..." align:#right height:16 + checkbox ChkMeshRecurse "Look in subfolders" checked:g_meshRecurse + checkbox ChkSmGroups "Smoothing groups" checked:g_fbxSmGroups + checkbox ChkSmMesh "Use FBX TurboSmooth" checked:g_fbxSmMesh + button BtnExportMeshes "Export meshes" + ) + + group "Animations" + ( + label Lbl20 "Converts all animations from currently" align:#left + label Lbl21 "loaded PSA to FBX format. A mesh should" align:#left + label Lbl22 "be loaded too. If multi-take FBX is not" align:#left + label Lbl23 "selected, each animation track will produce" align:#left + label Lbl24 "a separate FBX file." align:#left + checkbox ChkMultiTakeFbx "Save multi-take FBX file" checked:g_fbxMultiTake + button BtnExportAnims "Export animations" + ) + + on RadOutFormat changed state do g_outFormat = state + on ChkDefFbxSettings changed state do g_useDefaultFBXSettings = state + on ChkMultiTakeFbx changed state do g_fbxMultiTake = state + on BtnExportAnims pressed do ExportFbxAnim() + on BtnExportMeshes pressed do ExportFbxMeshes g_meshDir g_meshRecurse + + on EdMeshPath changed val do g_meshDir = val + on BtnBrowseMesh pressed do + ( + dir = getSavePath caption:"Directory for mesh lookup" initialDir:g_meshDir + if dir != undefined then + ( + g_meshDir = dir + EdMeshPath.text = dir + ) + ) + on ChkMeshRecurse changed state do g_meshRecurse = state + + on ChkSmGroups changed state do g_fbxSmGroups = state + on ChkSmMesh changed state do g_fbxSmMesh = state +) + +fn CreateExporterWindow = +( + local x = 300 + local y = 100 + local w = 250 + local h = 560 + + if fbxExportFloater != undefined do + ( + x = fbxExportFloater.pos.x + y = fbxExportFloater.pos.y + w = fbxExportFloater.size.x + h = fbxExportFloater.size.y + closeRolloutFloater fbxExportFloater -- close old window if visible + ) + fbxExportFloater = newRolloutFloater "FBX Batch Export" w h x y -- create new window + + addRollout fbxExportRollout fbxExportFloater +) + +CreateExporterWindow() + + + +------------------------------------------------------------------------------------------------------- + + -- START OF MASS CONVERT CODE + +------------------------------------------------------------------------------------------------------- + +/* +ActorX animation (psa) converter for 3ds Max + + Created: May 22 2021 + + Author: Aproydtix + + */ + +global fbxConvertFloater +fn GetDirs FolderPath Folders = () +fn GetListFromString InputString = () +fn SelectBones = () +fn ConvertTranslation = () +fn MassConvertPSA deformFix:false = () +fn CreateConverterWindow = () + +rollout fbxConvertRollout "ActorX Batch Convert" +( + -- copyright label + label Lbl1 "Version 1.4.1" + label Lbl2 "Modified by Aproydtix for KH3 modding" + label Lbl3 "Original Author: Konstantin Nosov (Gildor)" + hyperlink Lbl4 "http://www.gildor.org/" \ + address:"http://www.gildor.org/projects/unactorx" align:#center \ + color:black hovercolor:blue visitedcolor:black + + group "Convert" + ( + label Lbl5 "WARNING!" + label Lbl6 "Make sure to import your PSK first!" + label Lbl7 "This can take a while..." + checkbox ConvertToOneFolder "Convert to one folder" tooltip:"Puts all converted animations in the same folder." align:#center + button BtnMassConvertPsa "Batch-convert PSA ..." tooltip:"Converts all PSA files to FBX in the chosen folder and all its subfolders." + + + on BtnMassConvertPsa pressed do + ( + MassConvertPSA() + ) + ) + + group "Deformation Fix" + ( + label Lbl8 "Use Skin Pose on bones:" + edittext ListOfParentBones text:"atama" tooltip:"Uses the shape of the original model over the animation. Add bones for areas that get deformed." + checkbox CheckSelectChildren "Select children of bones" checked:true align:#center tooltip:"Whether to skin pose only the given bones or also their children." + checkbox CheckCenterOffset "Auto offset height" checked:true align:#center across:2 tooltip:"Offsets height difference based on start frame. Takes a bone to find height difference between models.\nCan be empty, using ground level as reference." + edittext OffsetBone text:"R_ashi2" tooltip:"Defaults to ground level if empty or bone can't be found. Recommended using R/L_ashi2." + label Lbl9 "Retain transform of bones:" + edittext ListOfRetainTransforms text:"R_buki, L_buki" tooltip:"Retains relative position, scale, and rotation of bones from the original animation. Very useful for detached bones like weapons." + + button BtnDeformFix "Fix deformations" tooltip:"Fixes the deformations on the given bones and their children." + button BtnMassDeformFix "Batch fix deform ..." tooltip:"While also fixing deformations, converts all PSA files to FBX in the chosen folder and all its subfolders.\nUses the same settings as Batch-convert." + + on BtnDeformFix pressed do + ( + undo on + ( + if SelectBones() do + ConvertTranslation() + ) + ) + + on BtnMassDeformFix pressed do + ( + MassConvertPSA deformFix:true + ) + ) +) + +fn GetListFromString InputString = +( + str = InputString + str = FilterString str "," + + for s = 1 to str.count do + ( + str[s] = trimLeft str[s] + str[s] = trimRight str[s] + ) + + return str +) + +fn SelectBones = +( + str = GetListFromString fbxConvertRollout.ListOfParentBones.text + Max select none + + for s = 1 to str.count do + ( + objName = str[s] + objPath = execute ("$'"+ objName + "'") + + if objPath == undefined then + ( + messageBox ("'" + objName + "' was not found!") + return false + ) + else + ( + selectMore objPath + if fbxConvertRollout.CheckSelectChildren.checked do + selectMore (execute ("$'"+ objName + "'...*")) + ) + ) + + return true +) + +fn ConvertTranslation = +( + -- Set Translation Mode and get Skin Pose + if (g_animTransMode != 1) do + ( + axAnimImportRollout.LstTransMode.selection = 1 + g_animTransMode = 1 + axLoadAnimation axAnimImportRollout.LstAnims.selection + ) + for o in selection do o.setSkinPose() + + -- Fix center position get values + centerPath = execute ("$'center'") + offsetObjPath = execute ("$'" + fbxConvertRollout.OffsetBone.text + "'") + isOffsetNull = false + if offsetObjPath == undefined do + isOffsetNull = true + startTime = animationRange.start.frame + endTime = animationRange.end.frame + posArr = #() + if isOffsetNull == false then + with animate on (at time startTime (centerOffset = centerPath.pos - offsetObjPath.pos)) + else + with animate on (at time startTime (centerOffset = centerPath.pos)) + for i=startTime to endTime do + with animate on (at time i (append posArr centerPath.pos)) + + + -- Retain bone position and rotation + retainNames = #() + retainPaths = #() + retainPos = #() + retainRot = #() + retainScale = #() + + str = GetListFromString fbxConvertRollout.ListOfRetainTransforms.text + for s = 1 to str.count do + ( + append retainNames str[s] + append retainPaths (execute ("$'"+ retainNames[s] + "'")) + + if retainPaths[s] == undefined then + ( + messageBox ("'" + retainNames[s] + "' was not found!") + return undefined + ) + else + ( + tempPos = #() + tempRot = #() + tempScale = #() + + for i = startTime to endTime do + with animate on (at time i + ( + append tempPos ( in coordsys parent retainPaths[s].pos ) + append tempRot ( in coordsys parent retainPaths[s].rotation ) + append tempScale ( in coordsys parent retainPaths[s].scale ) + )) + + append retainPos tempPos + append retainRot tempRot + append retainScale tempScale + ) + ) + + + -- Set Translation Mode and set Skin Pose + axAnimImportRollout.LstTransMode.selection = 2 + g_animTransMode = 2 + axLoadAnimation axAnimImportRollout.LstAnims.selection + for o in selection do o.assumeSkinPose() + + + -- Fix center position set values + if isOffsetNull == false then + with animate on (at time startTime (centerOffset -= centerPath.pos - offsetObjPath.pos)) + else + with animate on (at time startTime (centerOffset -= centerPath.pos)) + centerOffset = [0, 0, centerOffset.z] + if fbxConvertRollout.CheckCenterOffset.checked == false do + centerOffset = 0 + for i = startTime to endTime do + with animate on (at time i (centerPath.pos = posArr[i+1] - centerOffset)) + + + -- Retain position/rotation of Bones + for s = 1 to str.count do + ( + for i = startTime to endTime do + with animate on (at time i + ( + in coordsys parent retainPaths[s].pos = retainPos[s][i+1] + in coordsys parent retainPaths[s].rotation = retainRot[s][i+1] + in coordsys parent retainPaths[s].scale = retainScale[s][i+1] + )) + ) +) + +fn MassConvertPSA deformFix:false = +( + bones = FindAllBones() + if (bones.count == 0) then + ( + messageBox "Mesh is not loaded!" + return undefined + ) + + thePath = getSavePath() + if thePath != undefined do + ( + dirs = #(); + GetDirs thePath dirs + + for d in dirs do + ( + theFiles = getFiles (d+"\\*.psa") + for f in theFiles do + ( + AnimFileName = f + ImportPsaFile AnimFileName -1 + g_lastDir2 = getFilenamePath AnimFileName + axAnimImportRollout.LstAnims.items = for a in Anims collect (a.Name + " [" + (a.NumRawFrames as string) + "]") + + if deformFix then + ( + SelectBones() + ConvertTranslation() + + SetupFBX() + + local dir = getFilenamePath(AnimFileName) + "FBX" + "\\" + if fbxConvertRollout.ConvertToOneFolder.checked do + dir = thePath + "/FBX/" + makeDir dir all:true + + local track = Anims[1] + local trackName = track.Name + local filename = dir + trackName + ".fbx" + + exportFile (filename) #noPrompt + ) + else + ( + if fbxConvertRollout.ConvertToOneFolder.checked then + ExportFbxAnim givenPath:thePath + else + ExportFbxAnim() + ) + ) + ) + ) +) + +fn GetDirs FolderPath Folders = +( + for f in (getDirectories (FolderPath + "*")) do + ( + append Folders f + GetDirs f Folders + ) +) + +fn CreateConverterWindow = +( + local x = 570 + local y = 100 + local w = 250 + local h = 450 + + if fbxConvertFloater != undefined do + ( + x = fbxConvertFloater.pos.x + y = fbxConvertFloater.pos.y + w = fbxConvertFloater.size.x + h = fbxConvertFloater.size.y + closeRolloutFloater fbxConvertFloater -- close old window if visible + ) + fbxConvertFloater = newRolloutFloater "FBX Batch Convert" w h x y -- create new window + + addRollout fbxConvertRollout fbxConvertFloater +) + +CreateConverterWindow() diff --git a/README.md b/README.md index bd98c3e..469b6c4 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Some of these tools may be unmaintained and/or outdated, but are still useful fo The tag **[CU]** before a tool means that it is **C**ommonly **U**sed by multiple UE modding communities, so is highly recommended for personal use. +The tag **[PW]** before a tool means that it is **P**aywalled. + ## Packers & Unpackers Tools that deal with packing and unpacking the UE4 archive files. * [ue4pak](https://github.com/Vilsol/ue4pak) - Written by **Vilsol** @@ -47,7 +49,33 @@ Tools that deal with editing and parsing the UE4 asset files, with formats inclu - Open-source software for exploring Unreal Engine games' files. From seeing the properties of an asset to listening to your favorite audio files, it has never been easier to navigate inside a game's assets ## Textures, Animations & Modelling -Tools that deal with editing the textures, animations and models from UE4 games. +Tools that deal with editing the textures, animations and models from UE4 games. +**Short PSA:** To get custom materials working in later UE4 versions, go to `ProjectSettings` then to `Packaging` and set `ShareMaterialShaderCode` to `False`, then cook them like you normally would. +* [Blender3D Import PSK/PSA](https://github.com/Befzz/blender3d_import_psk_psa) - Written by **Befzz** + - Blender3D Import .psk & .psa addon imports meshes, skeletons and animations from .psk and .psa files to Blender3D +* **[CU]** [Blender3D Import PSK/PSA FORK](https://github.com/matyalatte/blender3d_import_psk_psa) - Written by **matyalatte** + - Automatically handles scaling/fbx export so its a bit more user friendly for not messing things up +* [UE4 DDS Tools](https://github.com/matyalatte/UE4-DDS-Tool) - Written by **matyalatte** + - Allows you to inject texture files directly into their original uassets without cooking for a large number of UE versions, and can do bulk operations via CLI +* [Rokoko Studio](https://github.com/Rokoko/rokoko-studio-live-blender) - Written by **Rokoko** + - Rokoko Studio is a powerful and intuitive software for recording, visualizing and exporting motion capture + - This plugin lets you stream your animation data from Rokoko Studio directly into Blender + - More useful for modding however It also allows you to easily record and retarget animations, for easy animation swaps +* **[PW]** [Better Blender FBX Importer/Exporter](https://blendermarket.com/products/better-fbx-importer--exporter) + - Better FBX Importer & Exporter is for people who need to import FBX files into Blender and export FBX files to game engines +* [Blender UEXP](https://github.com/AlexP0/Blender_UEXP) - Written by **AlexP0** + - Blender_UEXP creates a mesh in blender from a uexp to allow for edits, then writes modifications back into the uexp +* [Dummy Materials Blender Plugin](https://bleedn.gumroad.com/l/dummymaterials) - Written by **bleedn** + - Secondary DL link [here](https://www.artstation.com/marketplace/p/Jr02g/blender-dummy-materials-add-on) + - REQUIRES BLENDER 3.2 + - A plugin to automate making dummy materials + - Just click a face, set up the variables it asks for in the 3D View, and click the button +* [NVIDIA Texture Tools Exporter](https://developer.nvidia.com/nvidia-texture-tools-exporter) - Written by **NVIDIA** + - The NVIDIA Texture Tools Exporter allows users to create highly compressed texture files directly from image sources +* [3DSMax Bulk Export PSK/PSA to FBX](Loose Files\ActorXAnimConverter.ms) - Written by **Gildor**, adapted by **Aproydtix** + - Version of ActorX 3DSMax script that can bulk convert PSK/PSA to FBX + - [Original Script](https://www.gildor.org/projects/unactorx) by Gildor, and modifications by Aproydtix from OpenKH + - Also will do deformation fixes to put an animation on a modified skeleton ## .locres Editors