-- TPageBuilder_Zones.ms -- Andy Davis - Rockstar London -- February to March 2014 -- Cropping code for Texture Page Builder -- filein this struct from TPageBuilder_UI.ms ---------------------------------------------------------------------------------------------------------------------------- --ZoneStruct ---------------------------------------------------------------------------------------------------------------------------- struct ZoneStruct ( fn FormatObjectFaceList OFL = ( for item in OFL do ( format "%" (MaxOps.getNodeByHandle item.handle).Name format "\t%\n" item.FaceList ) ), fn GetUVZones ObjectFaceList &ZonesList = ( for item in ObjectFaceList do ( local handle = item.handle local obj = MaxOps.GetNodeByHandle item.handle local faceList = item.faceList for faceID in faceList do ( --get the UV bounds of this face local vertIDs = (polyop.GetVertsUsingFace obj faceID) as array local firstUV = RSPoly_GetSubVert_MapValue obj vertIDs[1] faceID 1 local minUV = [firstUV[1], firstUV[2]] local maxUV = [firstUV[1], firstUV[2]] numVerts = vertIDs.count for id = 2 to numVerts do ( local thisUV = RSPoly_GetSubVert_MapValue obj vertIDs[id] faceID 1 if thisUV[1] < minUV[1] then minUV[1] = thisUV[1] if thisUV[2] < minUV[2] then minUV[2] = thisUV[2] if thisUV[1] > maxUV[1] then maxUV[1] = thisUV[1] if thisUV[2] > maxUV[2] then maxUV[2] = thisUV[2] ) append ZonesList (UVZone (UVBounds minUV maxUV) (#(ObjectFaces handle #(faceID)))) ) ) ), --function tests whether two UVZone items intersect --zone1, zone2 - the input zones to match fn MatchZones zone1 zone2 fracture:false = ( local MatchFound = false --check if the U and V values match if (fracture) then ( if (zone1.UVBounds.UVMax[1] > zone2.UVBounds.UVMin[1]) then ( if (zone1.UVBounds.UVMin[1] < zone2.UVBounds.UVMax[1]) then ( if (zone1.UVBounds.UVMax[2] > zone2.UVBounds.UVMin[2]) then ( if (zone1.UVBounds.UVMin[2] < zone2.UVBounds.UVMax[2]) then MatchFound = true ) ) ) ) else ( if (zone1.UVBounds.UVMax[1] >= zone2.UVBounds.UVMin[1]) then ( if (zone1.UVBounds.UVMin[1] <= zone2.UVBounds.UVMax[1]) then ( if (zone1.UVBounds.UVMax[2] >= zone2.UVBounds.UVMin[2]) then ( if (zone1.UVBounds.UVMin[2] <= zone2.UVBounds.UVMax[2]) then MatchFound = true ) ) ) ) MatchFound ), -- ProcessUVZones creates temporary zone lists. this function manages the temporary list when a match is found --&NewZones is a reference to the current NewZones array --a and b are new additions to the NewZones array fn AppendNewZones &NewZones a b = ( NewZonesCount = NewZones.Count if NewZonesCount > 0 then ( local found = false for i = 1 to NewZonesCount do ( if (findItem NewZones[i] a > 0) or (findItem NewZones[i] b > 0) then ( AppendIfUnique NewZones[i] a AppendIfUnique NewZones[i] b found = true ) ) if not found then ( append NewZones #(a, b) ) ) else NewZones = #(#(a, b)) ), --adds a new ObjectFaceList item to an array of ObjectFaceList items fn AppendObjectFaceList &ObjectFaceList newItem = ( local found = false for item in ObjectFaceList do ( if (item.Handle == newItem.Handle) then ( item.FaceList = Sort (MakeUniqueArray (join item.FaceList newItem.FaceList)) found = true ) ) if (found == false) then append ObjectFaceList newItem ), -- function joins a group of zones into one zone item fn CondenseZones ZoneList idList = ( local CondensedZone = ZoneList[idList[1]] for id = 2 to idList.Count do ( local ThisZone = ZoneList[idList[id]] --extend the bounds if (ThisZone.UVBounds.UVMin[1] < CondensedZone.UVBounds.UVMin[1]) then CondensedZone.UVBounds.UVMin[1] = ThisZone.UVBounds.UVMin[1] if (ThisZone.UVBounds.UVMin[2] < CondensedZone.UVBounds.UVMin[2]) then CondensedZone.UVBounds.UVMin[2] = ThisZone.UVBounds.UVMin[2] if (ThisZone.UVBounds.UVMax[1] > CondensedZone.UVBounds.UVMax[1]) then CondensedZone.UVBounds.UVMax[1] = ThisZone.UVBounds.UVMax[1] if (ThisZone.UVBounds.UVMax[2] > CondensedZone.UVBounds.UVMax[2]) then CondensedZone.UVBounds.UVMax[2] = ThisZone.UVBounds.UVMax[2] --append the ObjectFaceList for item in ThisZone.ObjectFaceList do ( AppendObjectFaceList &CondensedZone.ObjectFaceList item ) ) CondensedZone ), --ZonesList: list of object-faces, and their respective uv bounds --returns UV zones fn ProcessUVZones ZoneList = ( local ContinueSort = true local Iteration = 0 --used as a debug failsafe because of the while loop while (ContinueSort and (Iteration < 100)) do ( Iteration += 1 --failsafe for while loop --collect an array of all the zones that should be grouped together into new zones local ZoneListCount = ZoneList.count local NewZoneGroups = #() --list of all the new groups of matched ZoneList items local NewZoneIDs = #() --list of all the new matched ZoneList items local UnchangedZoneIDs = #{} --list of the ids of zones in the ZoneList that have not been extended local UnchangedZones = #() --buffer for these unchanged ZoneList items local CondensedZones = #() --new zones created from condensing existing zones together --first, get the ZoneList ID's of Zones that should be condensed for i = 1 to (ZoneListCount - 1) do ( for j = (i + 1) to ZoneListCount do ( if (MatchZones ZoneList[i] ZoneList[j]) then ( AppendNewZones &NewZoneGroups i j AppendIfUnique NewZoneIDs i AppendIfUnique NewZoneIDs j ) ) ) --if we have not found any new zones, then the condensing process is complete if NewZoneIDs.Count == 0 then ContinueSort = false else ( --get a list of the ids for zones in the zone list that have not been condensed UnchangedZoneIDs = (for i = 1 to ZoneListCount where (findItem NewZoneIDs i == 0) collect i) for id in UnchangedZoneIDs do append UnchangedZones ZoneList[id] --create the new zones for item in NewZoneGroups do append CondensedZones (CondenseZones ZoneList item) --rebuild the ZoneList from the new zones and the old ones ZoneList = join UnchangedZones CondensedZones ) ) if (iteration == 100) then return "Error: UVZone iteration search limit exceeded" ZoneList ), --returns a rage folder with the given name and featuring the texturePath argument as the diffuse texture --candidate for common functions fn CreateRageDiffuseShader texturePath name tag:"Material" = ( local rageShader = Rage_Shader name: (tag + " " + name) local newBitmap = BitmapTexture name: ("Bitmap " + name) filename:texturePath RstSetShaderName rageShader "default.sps" RstSetVariable rageShader 1 newBitmap.filename showTextureMap rageShader true rageShader ), --candidate for common functions -- 1.7 million different random tags fn RandomTag = ( local alphabet = #("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z") local tag = alphabet[random 1 26] + alphabet[random 1 26] + alphabet[random 1 26] + (random 10 99) as string tag ), --returns a list of all objects using a particular material --candidate for common functions fn GetObjectsUsingMaterial inMaterial = ( local nodes = refs.dependents inMaterial local objs = #() for item in nodes where superClassOf item == GeometryClass do if item.material == inMaterial then append objs item objs ), --function adds a material to an existing material resulting in an appended multimaterial --returns the material id of the new material slot --candidate for common functions fn AppendMaterial baseMaterial newMaterial tag:"" = ( local newMatID if (classOf baseMaterial == MultiMaterial) then ( local numsubs = baseMaterial.numsubs baseMaterial.numsubs = numsubs + 1 baseMaterial[numsubs + 1] = newMaterial newMatID = baseMaterial.materialIDList[numsubs + 1] baseMaterial.Names[baseMaterial.materialIDList.count] = tag ) else ( local newMulti = MultiMaterial numsubs:2 name:(baseMaterial.name + " Multi") local objs = GetObjectsUsingMaterial baseMaterial newMulti[1] = baseMaterial newMulti.MaterialList[2] = newMaterialX newMulti.Names[2] = tag objs.material = newMulti addModifier objs (materialModifier materialid:1) convertToPoly objs newMatID = 2 ) newMatID ), --function checks zones for zero area zones --buffers them by the width of a pixel according to the input resolution fn CheckZones &UVZones resolution = ( local uNudge = 1.0 / resolution[1] --uv nudge distance equivalent to a pixel local vNudge = 1.0 / resolution[2] for item in UVZones do ( if (item.UVBounds.UVmin[1] == item.UVBounds.UVmax[1]) then ( if (item.UVBounds.UVmin[1] < 1) then item.UVBounds.UVmax[1] = item.UVBounds.UVmin[1] + uNudge else item.UVBounds.UVmin[1] = item.UVBounds.UVmin[1] - uNudge ) if (item.UVBounds.UVmin[2] == item.UVBounds.UVmax[2]) then ( if (item.UVBounds.UVmin[2] < 1) then item.UVBounds.UVmax[2] = item.UVBounds.UVmin[2] + vNudge else item.UVBounds.UVmin[2] = item.UVBounds.UVmin[2] - vNudge ) ) ), --gets the id of the material list number from a materials matID value (for the case of missing materials) fn GetMatListIDFromMatID multimat matid = ( local matListID = 0 local numMaterialIDs = multimat.materialidlist.count local found = false for i = 1 to numMaterialIDs while not found do ( if multimat.materialidlist[i] == matid then ( matListID = i found = true ) ) matListID ), --function applies one new multimaterial to a selection of editable poly objects --candidate for common functions --converts single materials into multimaterials --polyobjects - array of objects to be merged --if you send a single object in an array, the function applies an optimised multimaterial --works without modifiers --polyobjects: array of editable poly objects --27/02/14: updated to respect name field --13/03/14: updated to fix out of range matID's --nb dependency on GetMatListIDFromMatID fn MergeMultiMaterials polyobjects = ( local MaterialList = #() --list of materials local NameList = #() --get all the materials into the MaterialList and set the material IDs for item in polyobjects do ( local thisMat = item.material local faceCount = item.faces.count if (classof item.material == MultiMaterial) then ( for i = 1 to faceCount do ( local matID = polyop.getFaceMatID item i --check if matID legal: if not then fix it if matID > item.material.count then ( local correctedValue = mod matID item.material.count if (correctedValue == 0) then correctedValue = item.material.count polyop.setFaceMatID item i correctedValue matID = correctedValue ) local mat = thisMat[matID] local newMatID = findItem MaterialList mat if (newMatID == 0) then ( local matListID = this.GetMatListIDFromMatID thisMat matID --the number in meditMaterials list append MaterialList mat append NameList thisMat.Names[matListID] newMatID = MaterialList.count ) polyop.SetFaceMatID item i newMatID ) ) else ( local newMatID = findItem MaterialList thisMat if (newMatID == 0) then ( append MaterialList thisMat append NameList "" newMatID = MaterialList.count ) for i = 1 to faceCount do polyop.SetFaceMatID item i newMatID ) ) --create the multimaterial local multiName = "Multimaterial: " if (polyobjects.count == 1) then multiName += polyobjects[1].name else multiName += RandomTag() local numMaterials = MaterialList.count local newMultiMaterial = MultiMaterial numsubs:numMaterials name:multiName for i = 1 to numMaterials do ( newMultiMaterial[i] = MaterialList[i] newMultiMaterial.Names[i] = NameList[i] ) --apply the multimaterial polyobjects.material = newMultiMaterial ), fn SplitZones InputTD Apply:true = --new smaller textures are created that match the UV-zones --definition of UV zone: a collection of UV elements that fit within a shared bounding box --InputTD: texture definition --returns: texture definitions representing the zones generated from the InputTD, and a list of the new materials --note that this function creates textures in the temporary folder, so not suitable for check in --the reason for this is that the cropped textures are designed to be for temporary use only prior to texture page building ( local ObjectFaceList = #() local numObjs = InputTD.ObjectFaceList.Count local UVZonesList = #() --master array containing all the UVZones and corresponding ObjectFace lists local resolution = InputTD.GetResolution() local CroppedMaterials = #() local CroppedTextures = #() local OutputTDs = #() --get the ObjectFaceList for the current TextureDefinition for i = 0 to (numObjs - 1) do ( local handle = InputTD.ObjectFaceList.Item[i].Handle local faceList = InputTD.ObjectFaceList.Item[i].FaceList.ToArray() append ObjectFaceList (DataPair handle:handle faceList:faceList) ) --get into the uv data and calculate the zones from the ObjectFaceList this.GetUVZones ObjectFaceList &UVZonesList --calculate the UV zones UVZones = this.ProcessUVZones UVZonesList this.CheckZones &UVZones resolution -- TPageTool.FormatTextureDefinition InputTD --dev --create the new cropped textures if (UVZones.count == 1 and UVZones[1].UVBounds.uvmin == [0,0] and UVZones[1].UVBounds.uvmax == [1,1] ) then OutputTDs = #(InputTD) --no cropping this texture else ( rollout temprollout "Texture Page Builder" ( label myLabel "Cropping Textures" ) theNewFloater = createDialog temprollout 200 30 TPageTool.TPageOps.StopRedraw() for item in UVZones do ( --create the new cropped texture local umin = item.UVBounds.UVmin[1] local vmin = item.UVBounds.UVmin[2] local umax = item.UVBounds.UVmax[1] local vmax = item.UVBounds.UVmax[2] local info = InputTD.Path + "\n" + umin as string + "\n" + vmin as string + "\n" + umax as string + "\n" + vmax as string -- messagebox info --dev local newTexturePath = TPageTool.TPageOps.TPageBuilder.CropImage InputTD.Path umin vmin umax vmax append CroppedTextures newTexturePath if (Apply) then ( local newShader = this.CreateRageDiffuseShader newTexturePath (RandomTag()) tag:"Cropped" append CroppedMaterials newShader --add the texture to the multimaterial --all the objectFaces will use the same material, so just pick the first object local firstObject = maxOps.getNodeByHandle (item.ObjectFaceList[1].handle) local newMatID = this.AppendMaterial firstObject.material newShader tag:"cropped" --loop throught the object faces, assigning the new texture and remapping the UV's for objectFace in item.ObjectFaceList do ( --select the faces local obj = maxOps.getNodeByHandle objectFace.handle local UVXFormMod = UVW_XForm() --assign the texture polyop.setFaceMatID obj (objectFace.faceList as bitarray) newMatID --set the uv's for each face --to transform the faces, we need to translate each face by -(umin, vmin) to set to origin --then scale each face to expand to fill the cropped texture by (1/(umax - umin), 1/(vmax - vmin)) max modify mode --necessary for manual operations select obj --must select the objects individually or the modifier gets added to all the objects SubObjectLevel = 4 obj.editablepoly.SetSelection #Face #{} --clear current face selection obj.editablepoly.SetSelection #Face (objectFace.faceList as bitarray) --select the faceList for this texture --we have to add the UVW_XForm modifiers separately, as we can't control the transformation order ModPanel.AddModToSelection (UVW_XForm U_Offset:-umin V_Offset:-vmin) ModPanel.AddModToSelection (UVW_XForm U_Tile: (1/(umax - umin)) V_Tile: (1/(vmax - vmin))) ConvertToPoly obj SubObjectLevel = 0 ) TPageTool.TPageOps.ShowTextures true ) --calculate new TextureDefinitions from the UVZones: each zone gets a new TD append OutputTDs (TPageTool.CreateTextureDefinition newTexturePath item.ObjectFaceList) ) TPageTool.TPageOps.ResumeRedraw() destroyDialog temprollout ) OutputTDs ) ) ---------------------------------------------------------------------------------------------------------------------------- --TEST ---------------------------------------------------------------------------------------------------------------------------- /* ZoneOps = ZoneStruct() polyobjs = for item in selection where SuperClassOf item == GeometryClass collect item --assign a single material to all the affected objects RSPoly_MergeMultiMaterials polyobjs --build a TextureDefinitionList (TDL) TPageOps = TPageStruct() TPageOps.BuildTextureDefinitionList polyobjs TPageOps.TPageBuilder.ProcessTextureDefinitionList() --adds texture resolutions and sorts TDList = TPageOps.TPageBuilder.TextureDefinitionArray clearListener() --split each TD into zones for item in TDList do ZoneOps.SplitZones item --optimise the multimaterials for item in polyobjs do RSPoly_MergeMultiMaterials #(item) select polyobjs */