Last active
April 11, 2023 11:51
-
-
Save Buckminsterfullerene02/12947999641c6a290f2cbbaf4e0ee313 to your computer and use it in GitHub Desktop.
Written by Gildor, adapted by Aproydtix, further modified by Buckminsterfullerene
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 | |
( | |
ClearMaxScene() | |
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_massExportAnims | |
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_massExportAnims == undefined) then g_massExportAnims = true | |
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 -- <bool> | |
-- Controls the state of the "Geometries" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "NormalsPerPoly" true -- <bool> | |
-- Controls the state of the "Support normals per polygon vertex" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "Cameras" false -- <bool> | |
-- Controls the state of the "Cameras" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "Lights" false -- <bool> | |
-- Controls the state of the "Lights" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "GeomAsBone" true -- <bool> | |
-- Controls the state of the "Geometries used as bones, exported as bones" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "Shape" false -- <bool> | |
-- Controls the state of the "Shape (Morph modifier)" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "Skin" true -- <bool> | |
-- Controls the state of the "Skins (Skin Modifier and Physique)" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "Animation" true -- <bool> | |
-- Controls the state of the "Animation" checkbox in the FBX Export dialog. | |
-- FbxExporterSetParam "Resampling" -- <float> | |
-- Controls the value of the "Resampling rate (when necessary)" field in the FBX Export dialog. | |
FbxExporterSetParam "ShowWarnings" false -- <bool> | |
-- Controls the state of the "Show warnings" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "EmbedTextures" false -- <bool> | |
-- Controls the state of the "Embed textures in export file" checkbox in the FBX Export dialog. | |
FbxExporterSetParam "SmoothingGroups" g_fbxSmGroups -- <bool> | |
-- True or false. See Smoothing Groups for an explanation of this setting. | |
FbxExporterSetParam "SmoothMeshExport" g_fbxSmMesh -- <bool> | |
-- 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) + "\\" | |
if givenPath != "" do | |
dir = givenPath | |
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) | |
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 | |
-- batch convert PSA inside of current dir | |
if (g_massExportAnims == true) then ( | |
ClearMaxScene() | |
ImportPskFile psk_filename | |
MassConvertPSA dir true false | |
) | |
-- if the fbx mesh already exists in the directory, skip it | |
local fbxFilename = filename + ".fbx" | |
if (not FileExists fbxFilename) then | |
( | |
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() | |
) | |
fn FileExists fname = (getfiles fname).count != 0 | |
-- 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 Lvl25 "Exports animations - Buckminsterfullerene" align:#left | |
checkbox ChkExportAnims "Mass export animations" checked:g_massExportAnims | |
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 ChkExportAnims changed state do g_massExportAnims = 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.5.0" | |
label Lbl2 "Originally modified by Aproydtix for KH3 modding" | |
label Lb25 "Modified batch convert by Buckminsterfullerene" | |
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 "" false false | |
) | |
) | |
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 "" false 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 PSKDir isBatchExportPSK deformFix = | |
( | |
bones = FindAllBones() | |
if (bones.count == 0) then | |
( | |
messageBox "Mesh is not loaded!" | |
return undefined | |
) | |
thePath = "" | |
if isBatchExportPSK then | |
( | |
-- take the "\" out of the end of the path | |
dir2 = trimRight PSKDir "\\" | |
thePath = dir2 | |
) | |
else | |
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) + "\\" | |
if fbxConvertRollout.ConvertToOneFolder.checked do | |
dir = thePath + "/FBX/" | |
makeDir dir all:true | |
local track = Anims[1] | |
local trackName = dir + track.Name | |
local filename = 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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment