Sunday, 16 March 2014

Fix nGons

Long time no post... A full year in fact! Work has kept me busy, and I've come on a lot with maxscript writing, but haven't had much time (or need!) to write any outside of work. I'm working on a big tool, more info later.

But for now, here's a script to try and correct ngons. It's got a few problems, I haven't had time to debug. Also you'll notice it's a bit of a monster script and for anyone trying to follow along... good luck, you'll need it!

Important: There are some instances where this script crashes max. I have not fully debugged the cause yet. Use with caution!

USAGE

  1. Select one or more objects
  2. run script


* there seems to be some cases where it doesn't completely finish. Run it again if so.


  


( 
 
 local CurrentObj
 local CurrentFace
 
 local FirstVert
 
 local faceedges = #()
 local faceedgeverts = #()
 local faceverts = #()
 local facevertedges = #()
 local vertneighbourverts = #()
 
 local StructFaceVerts = #()
 local StructVertConnections = #()
 
 
 fn ConnectAndWalk v1 v2 innerverts =
 (
  --format "ConnectAndWalk: %, %, %\n" v1 v2 innerverts
  nextv1 = ((vertneighbourverts[v1] - innerverts) as array)[1]
  nextv2 = ((vertneighbourverts[v2] - innerverts) as array)[1]
  if nextv1 == v2 or nextv2 == v1 then return true --end of the line, punk!
  
  polyop.createedge CurrentObj v1 v2
  
  -- this has become so ridiculously abstract, i guarantee i wont understand this when i look back. OH HEY FUTURE MIKE!
  -- if the neighbouring verts of the next verts have none in common, we can connect
  if ((vertneighbourverts[nextv1] * vertneighbourverts[nextv2])).numberset == 0 then
   (ConnectAndWalk nextv1 nextv2 #{v1,v2})
  return true
 )
 
 
 
 struct StructVertConnection
 (
  v1,
  v2,
  --FormsTriangle,
  --FormsQuad,
  InsideVerts,
  VertDistance,
  
  on create do
  (
   VertDistance = distance (polyop.getvert CurrentObj v1) (polyop.getvert CurrentObj v2)
  )
  
 )


 fn BuildConnectionsFromVert v1 v2 ed  =
 (
  NextEdge = ((facevertedges[v2] - facevertedges[v1]) as array)[1]
  NextVert = ((faceedgeverts[NextEdge] - faceedgeverts[ed]) as array)[1]
  
  vertneighbourverts[v1][v2] = true
  vertneighbourverts[v2][v1] = true
  
  if faceverts.count <= 5 then --we only care about tri-making connections if we have a 5-sided 
   (append StructVertConnections (StructVertConnection v1 NextVert InsideVerts:#{v2})) --  FormsTriangle:true FormsQuad:false  (these work but arent needed under new system)
  
  NextNextEdge = ((facevertedges[NextVert] - facevertedges[v2]) as array)[1]
  NextNextVert = ((faceedgeverts[NextNextEdge] - faceedgeverts[NextEdge]) as array)[1]
  
  --FormsQuad = if faceverts.count <= 5 then false else true
  append StructVertConnections (StructVertConnection v1 NextNextVert InsideVerts:#{v2, NextVert}) --  FormsTriangle:(not FormsQuad) FormsQuad:FormsQuad (these work but arent needed under new system)
  
  if v2 == FirstVert then return true -- end recursion
  BuildConnectionsFromVert v2 NextVert NextEdge
  
  return true
 )



 fn FixFaceNgons obj f =
 (
  
  
 
 faceedges = #()
 faceedgeverts = #()
 faceverts = #()
 facevertedges = #()
 vertneighbourverts = #()
 
 StructFaceVerts = #()
 StructVertConnections = #()
  
  
  
  CurrentObj = obj
  CurrentFace = f
  
  faceverts = polyop.getFaceVerts obj f
  if faceverts.count < 5 then return false
  facevertsba = faceverts as bitarray
  
  faceedges = polyop.getFaceEdges obj f
  faceedgesba = faceedges as bitarray
  
  faceedgeverts = #()
  facevertedges = #()
  vertneighbourverts = #()
  
  for v in faceverts do facevertedges[v] = ((polyop.getEdgesUsingVert obj v) as bitarray) * faceedgesba
  for ed in faceedges do faceedgeverts[ed] = ((polyop.getVertsUsingEdge obj ed) as bitarray) * facevertsba
  for v in faceverts do vertneighbourverts[v] = #{} -- we populate this later
  
  StructFaceVerts = #()
  StructVertConnections = #()
  
  firstedgeverts = for v in faceedgeverts[faceedges[1]] collect v
  FirstVert = firstedgeverts[1]
  
  StructVertConnections = #()
  BuildConnectionsFromVert firstedgeverts[1] firstedgeverts[2] faceedges[1]
  
  --for vc in StructVertConnections do (print vc)
  
  shortestvc = StructVertConnections[1]
  for vc = 2 to StructVertConnections.count do
  (
   if StructVertConnections[vc].VertDistance < shortestvc.VertDistance then 
    (shortestvc = StructVertConnections[vc])
  )
  
  ConnectAndWalk shortestvc.v1 shortestvc.v2 shortestvc.InsideVerts
  
  return false
  
 )
  
  

fn FixAllNgons obj =
(
 --disablesceneredraw()
 polyop.retriangulate obj #{1..(polyop.getnumfaces obj)}
 f = 0
 while 1 == 1 do 
 (
  f += 1
  if (FixFaceNgons obj f) then f = 0
  if f >= (polyop.getnumfaces obj) then exit
 )
 enablesceneredraw()
 completeredraw()
)

fn FixAllConcave obj =
(
)



fn FixSelectedObjectsNgons =
(
 usersel = getcurrentselection()
 for o in usersel where superclassof o == geometryclass do
 (
  if classof o != editable_poly then
  (
   messagebox (o.name + " is not an editable poly")
   continue
  )
  
  FixAllNgons o
  FixAllConcave o
 )
)

FixSelectedObjectsNgons()

)

  

Wednesday, 3 April 2013

Splay Normals

Foliage models often require edited normals. There are already normal-splaying scripts out there but I wanted to be able to set a position and use a strength value to effectively splay them partly from the centre of the "bush", and partly from the branch or trunk. I also wanted to be able to splay only the selected verts. This is the result.

USAGE
  • Position the working pivot
  • optionally select some of the verts (if none selected, will select all)
  • optionally set strength to a value like 0.4
  • run script



  
-- MixeScript snippet: SplayNormals
-- Splays selected normals away from the working pivot

--USAGE
-- Position the working pivot
-- select some or all of the verts (if none selected, will select all)
-- optionally set strength to a value like 0.4
-- run script
 
--SETTINGS
Strength = 1.0 -- when set to 1.0, entirely replaces existing normal direction. at 0.0, does not adjust normal at all.

max modify mode
obj = $
modPanel.setCurrentObject obj
enMods = (for m in obj.modifiers where classof m == editnormals collect m)
enMod = enMods[1]
 
subobjectLevel = 1 --verts 
vertarray = #{}
vertarray = polyop.getVertSelection obj
if vertarray.numberSet == 0 then -- no selection, select all
(
 polyop.setVertSelection obj #all
 vertarray = polyop.getVertSelection obj
)


if enMod == undefined then
(
 enMod = editnormals()
 modPanel.addModToSelection (enMod) ui:on
)

 
WPTM = WorkingPivot.getTM()
splayfrom = (WPTM[4])
modPanel.setCurrentObject obj

normarray = enMod.GetSelection()
--enMod.MakeExplicit ()
objpos = obj.pos

modPanel.setCurrentObject enMod
  
for vert in vertarray do (
 vertpos = (enMod.GetVertex vert node:obj) + objpos
 newnormangle = normalize (vertpos - splayfrom)
 tempbitarray = #{vert}
 norms = #{}
 enMod.ConvertVertexSelection tempbitarray norms
 enMod.MakeExplicit selection:norms
 for norm in norms do 
 (  
  existingnormangle = normalize (enMod.GetNormal (norm))
  finalangle = normalize ( newnormangle * Strength + existingnormangle * (1 - Strength))
  enMod.SetNormal norm finalangle
 )
)
  

Wednesday, 27 March 2013

Fix Meshes

Lots of users experience issues with their meshes. This script provides a convenient way to do all of the common mesh-fixing steps.

USAGE
  • Select one or more geometry objects
  • run script
  • choose from any of the below buttons
    • Reset Xform - Resets selected objects XForms
    • Reset Normals - applies an edit normals modifier and resets all of the normals (so that they are again controlled by smoothing groups)
    • Reset Object - creates a new box object and attaches the existing mesh to it, then deletes the temporary box.. This essentially wipes all object properties. material, wire colour and pivot are all retained.
    • New Scene - exports the selected geometry, creates a new scene and imports it again. deletes the FBX file when done.
    
-- Mixescript snippet: Fix Objects
-- attempts to correct common problems with meshes

-- USAGE
-- select the bad meshes
-- run script
-- choose any of the buttons. I recommend working top to bottom
 
rollout rol_FixObject "Fix Object" width:104 height:168
(
 button btn_ResetXForm "Reset XForm" pos:[8,40] width:88 height:24
 button btn_ResetNormals "Reset Normals" pos:[8,72] width:88 height:24
 button btn_ResetObject "Reset Object" pos:[8,104] width:88 height:24
 button btn_NewScene "New Scene" pos:[8,136] width:88 height:24
 label lbl1 "Warning: will collapse stack" pos:[16,8] width:72 height:32
 
 on btn_ResetXForm pressed do
 (
  undo on 
  (
   objarray = getcurrentselection()
   for obj in objarray do 
   (
    ResetXform obj
    ConvertTo obj Editable_poly
   )
   select objarray
  )
 )
 
 on btn_ResetObject pressed do
 (
  undo on 
  (
   max modify mode
   objarray = getcurrentselection()
   newsel = #{}
   for i = 1 to objarray.count do 
   (
    obj = objarray[i]
    fixedobj = Box name:obj.name lengthsegs:1 widthsegs:1 heightsegs:1 length:1 width:1 height:1 mapcoords:on pos:[0,0,0] isSelected:off
    fixedobj.pivot = obj.position
    fixedobj.wirecolor = obj.wirecolor
    fixedobj.mat = obj.mat
    ConvertTo fixedobj Editable_poly
    polyop.attach fixedobj obj
    polyop.deleteverts fixedobj #{1..8} --delete original box
    objarray[i] = fixedobj
   )
   
   select objarray
  )
 )
 
 on btn_ResetNormals pressed do 
 (
  undo on 
  (
   max modify mode 
   objarray = getcurrentselection()
   for obj in objarray do 
   (
    ConvertTo obj Editable_poly
    select obj
    modPanel.setCurrentObject obj
    polyop.setVertSelection obj #all
    allverts = polyop.getVertSelection obj
    allnormals = #{}
    enMod = Edit_Normals ()
    modPanel.addModToSelection (enMod) ui:on
    enMod.ConvertVertexSelection allverts allnormals
    enMod.Reset selection:allnormals
    ConvertTo obj Editable_poly
   )
   select objarray
  )
 )
 
 
 on btn_NewScene pressed do 
 (
  --checks
  cancel = false
  if selection.count == 0 then (messagebox ("please select one or more objects"); cancel = true)
  if maxfilepath == "" then (messagebox ("please save your scene first."); cancel = true)
  
  if cancel == false then (
   ContinueScript = queryBox "This will create a new scene and only copy across geometry. Unsaved changes will be lost. Continue?"
   if ContinueScript then
   (
    exportpath = maxfilepath + maxfilename + ".FBX"
    exportFile exportpath #noPrompt selectedOnly:true
    actionMan.executeAction 0 "16" --new scene
    --actionMan.executeAction 0 "40005" -- reset scene -switch this out for above line if you want to keep materials etc. this is essentially less aggressive.
    importFile exportpath #noPrompt
    deleteFile exportpath
   )
  )
 )
 
)

CreateDialog rol_FixObject NewDlg
  

Monday, 25 March 2013

Snap to nearest gridpoint

By request, a script to move selected vertices to the nearest gridpoint. I also threw in snapping selected objects to nearest gridpoint.




Usage
  • Select verts of an editable poly or select objects
  • run script
  • verts/objects will be at nearest gridpoints. (note: uses objects pivot)
  
-- MixeScript snippet: Snap to Nearest Grid Point
-- Snaps selected verts or objects to nearest grid point

--USAGE
-- Select verts of an editable poly or select objects
-- run script
-- choose button to snap to
-- verts/objects will be at nearest gridpoints. (note: uses objects pivot)

fn findnearestgridpoint frompoint axis = (
 spacing = getGridSpacing()
 
 nearestx = frompoint.x
 nearesty = frompoint.y
 nearestz = frompoint.z
 
 nearestx = (floor (frompoint.x / spacing + 0.5)) * spacing
 nearesty = (floor (frompoint.y / spacing + 0.5)) * spacing
 nearestz = (floor (frompoint.z / spacing + 0.5)) * spacing
 
 if axis == "x" then ( nearesty = frompoint.y; nearestz = frompoint.z)
 if axis == "y" then ( nearestx = frompoint.x; nearestz = frompoint.z)
 if axis == "z" then ( nearesty = frompoint.y; nearestx = frompoint.x)
  
 return [nearestx,nearesty,nearestz]
)

fn DoSnap axis = 
(
 if subobjectlevel == 1 then 
 ( 
   obj = $
   vertselection = polyop.getVertSelection obj
  max modify mode
  for vert in vertselection do (
   polyop.setVert obj vert (findnearestgridpoint (polyop.getVert obj vert) axis)
  )
 ) else (
  objarray = getcurrentselection()
  for obj in objarray do (
   obj.pos = findnearestgridpoint obj.pos axis
  )
 )
)

rollout rol_SnapToGrid "Snap To Grid Points" width:136 height:48
(
 button snapX "X" pos:[8,24] width:24 height:19
 label lbl1 "Snap to Grid Points" pos:[24,5] width:96 height:16
 button snapY "Y" pos:[40,24] width:24 height:19
 button snapZ "Z" pos:[72,24] width:24 height:19
 button SnapAll "All" pos:[104,24] width:24 height:19
 
 on snapX pressed do (undo on (DoSnap "x"))
 on snapY pressed do (undo on (DoSnap "y"))
 on snapZ pressed do (undo on (DoSnap "z"))
 on SnapAll pressed do (undo on (DoSnap "all"))
)

createDialog rol_SnapToGrid dlg_SnapToGrid
  

Sunday, 24 March 2013

MixeScript Roundup 1

Thought I'd show in video what the scripts posted up to now do. Also I've whacked them all in a zip if you want to use them as buttons in your interface.







Colour Objects (by random/by position)

I needed a script that would give a random colour to my leaves and also give a colour to them based on height so that I could use this to make the leaves at the back disappear first in autumn.

For the sake of functionality, I gave it three purposes:

Usage:
  • select many objects
  • adjust settings between height by colour / random colour / random greyscale
  • run script
Update:
  • I needed this to give an even distribution of colours, so introduced a new method which sorts based on position, then assigns a colour based on sort order. slightly long-winded code, sorry!
  
  
-- MixeScript snippet: Colour Objects (random/by pos)
-- Removes material from objects and applies a random colour, or colour based on their position.

--USAGE
-- select many objects
-- adjust settings between height by colour / random colour / random greyscale
-- run script

--SETTINGS
useposcolour = true --if false, uses a random colour.
greyscale = false -- if true then r=g=b (grey tones)
sortbasedcolouring = false -- if true, sorts objects then colours based on sort order. ensures even distribution of colours and no matching colours.
 
 
if sortbasedcolouring == false then
(
 minx = $.min.x
 maxx = $.max.x
 xdifference = maxx - minx
 miny = $.min.y
 maxy = $.max.y
 ydifference = maxy - miny
 minz = $.min.z
 maxz = $.max.z
 zdifference = maxz - minz
 sort 

 for o in $ do (
  o.mat = undefined
  
  xcolour = (o.center.x - minx) / xdifference * 255
  ycolour = (o.center.y - miny) / ydifference * 255
  zcolour = (o.center.z - minz) / zdifference * 255 
  
  if useposcolour then (
   r = zcolour ;  g = zcolour ;  b = zcolour
  ) else (
   if greyscale then (
    r = random 0 255 ;  g = r ;  b = r
   ) else (
    r = random 0 255 ;  g = random 0 255 ;  b = random 0 255
   )
  )
  
  o.wirecolor = [r,g,b]
 )
)
else 
(
 
 mysel = getcurrentselection()
  
 fn CompareX newobj prevobj =
 ( 
  case of
  (
   (newobj.center.x >  prevobj.center.x): 1
   (newobj.center.x <  prevobj.center.x): -1
   default:0
  )
 )
  
 fn CompareY newobj prevobj =
 ( 
  case of
  (
   (newobj.center.y >  prevobj.center.y): 1
   (newobj.center.y <  prevobj.center.y): -1
   default:0
  )
 )

 fn CompareZ newobj prevobj =
 ( 
  case of
  (
   (newobj.center.z >  prevobj.center.z): 1
   (newobj.center.z <  prevobj.center.z): -1
   default:0
  )
 )

 selsize = mysel.count
 --print ("SELSIZE = " + (selsize as string))

 qsort mysel CompareX
 for i = 1 to selsize do ( 
  newcolour = (((i-1.0) / (selsize-1.0) * 255) as string)
  mysel[i].wirecolor = [(newcolour as integer),mysel[i].wirecolor.g,mysel[i].wirecolor.b]
  --print ("XVALUE: i = " + (i as string) + " , colour = " + (mysel[i].wirecolor as string))
 )

 qsort mysel CompareY
 for i = 1 to selsize do (
  newcolour = (((i-1.0) / (selsize-1.0) * 255) as string)
  mysel[i].wirecolor = [mysel[i].wirecolor.r,(newcolour as integer),mysel[i].wirecolor.b]
  --print ("YVALUE: i = " + (i as string) + " , colour = " + (mysel[i].wirecolor as string))
 )

 qsort mysel CompareZ
 for i = 1 to selsize do (
  newcolour = (((i-1.0) / (selsize-1.0) * 255) as string)
  mysel[i].wirecolor = [mysel[i].wirecolor.r,mysel[i].wirecolor.g,(newcolour as integer)]
  --print ("ZVALUE: i = " + (i as string) + " , colour = " + (mysel[i].wirecolor as string))
 )

 --optionally, use this to greyscale.
 --for o in mysel do (o.wirecolor = [o.wirecolor.b,o.wirecolor.b,o.wirecolor.b])
)
  

Friday, 22 March 2013

Fix Intersections

A common problem when using things like object paint and scatter are intersecting objects - many of your objects cutting through one another.

USAGE
  • select many objects
  • adjust intersection aggressiveness setting (between 0.1 and 1.0)
  • run script
  • you are left with a selection
  • delete the selection, or rotate or scale them or whatever fix is most appropriate.
LIMITATIONS
  • Intersection detection is simple bounding box intersection. Therefore does not work particularly well if bounding box is not a good fit for the object. If I have cause, I'll introduce a sphere/squashed-sphere based intersection detection at a later date.



  
-- MixeScript snippet: Select Intersections
-- selects objects which intersect one another by performing overlapping bounding box detection. optional adjustment for bounding box downscaling.

--USAGE
--select many objects
--adjust intersection aggressiveness setting (between 0.1 and 1.0)
--run script
--you are left with a selection
--delete the selection, or rotate or scale them or whatever fix is most appropriate.

--SETTINGS
IntersectionAggressiveness = 0.5 -- downscales objects to center. choose between 0.1 and 1.0 (1.0 uses bounding boxes exactly as they are)


Global IsIntersecting = #()
Global PivotPositions = #()
Global IntersectingObjects = #()

fn DownScale objarray = (
 if IntersectionAggressiveness != 1.0 then (
  for o in objarray do (
   append PivotPositions o.position
   o.pivot = o.center
   o.scale = o.scale * [IntersectionAggressiveness,IntersectionAggressiveness,IntersectionAggressiveness]
  )
 )
)

fn UpScale objarray = (
 i = 0
 if IntersectionAggressiveness != 1.0 then (
  for o in objarray do (
   i=i+1
   o.scale = o.scale / [IntersectionAggressiveness,IntersectionAggressiveness,IntersectionAggressiveness]
   o.pivot = PivotPositions[i]
  )
 )
 Global PivotPositions = #()
)


objarray = getcurrentselection()
DownScale(objarray)

--Detect intersections
for i = 1 to objarray.count do (
 doesintersect = false
 for y = 1 to objarray.count do ( 
  if y != i and IsIntersecting[y] == false then --ignore same object and objects already detected as intersecting
  (
   if (intersects objarray[i] objarray[y]) == true then (doesintersect = true)
  )
 )
 append IsIntersecting doesintersect
 if doesintersect then (append IntersectingObjects objarray[i])
)

UpScale(objarray)

select IntersectingObjects