I’m not sure I could figure it out, or get it working fast enough. I also just like hearing things like, use setFlags or whatever, instead of setVert, which is faster? Little tricks like that are cool.
- applies to me, but also
- flat out with work at the moment…
If there’s something that I think I can have a crack at I will… Maybe when the challenge is ‘move a selected object using maxscript’
However one can learn by watching, taking notes, and slowly, painfully understanding…
for me it’s mostly #5: too busy with work at the moment. same reason for not participating in the first challenge. In a couple weeks things should calm down again and I will participate if it’s still going on by then!
Also re this particular challenge a little bit of #3 applies since I’ve never personally needed to do this
I’m finding it very very interesting but it’s pretty advanced, and unless you’ve got the time to spend analysing what’s going on it’s difficult to contribute. The code is quite long, could the next challenge be on a simpler function?
It is very interesting and i’m definitely going to have to sit down and learn these advanced maxscripting lessons.
I have merged Denis’s optimization into the complete code and added a smoothing algorithms that isn’t doing a good job yet and dramatically slowing down the whole process.
Anyone has any idea how to do the smoothing faster / better?
(
struct s_VertData
(
index,
OriginalNormals = #()
)
struct s_EdgeData
(
owner,
index,
verts,
midVert,
fn divide =
(
midVert = owner.divideEdge index 0.5
-- format "Edge % divided
" index
),
fn init =
(
verts = polyop.getEdgeVerts owner index
not polyop.isEdgeDead owner index
),
valid = init()
)
struct s_NormalDataLight
(
pos,
dir,
weight
)
struct s_NormalData
(
owner,
faceIndex,
Vert,
index,
dir,
pos,
weight = 1.0,
fn init =
(
local faceNorms = #{}
local vertNorms = #{}
owner.norms.ConvertFaceSelection #{faceIndex} faceNorms
owner.norms.ConvertVertexSelection #{Vert.index} vertNorms
faceNorms = faceNorms as array
vertNorms = vertNorms as array
for i in faceNorms where findItem vertNorms i > 0 do exit with index = i
if index != undefined then (
dir = owner.norms.getNormal index
pos = owner.norms.getVertex Vert.index
true
) else
false
),
fn lineLineIntersect pA a pB b =
(
local c = pB - pA
local cross1 = cross a b
local cross2 = cross c b
pA + (a * ((dot cross2 cross1) / ((length cross1)^2)))
),
fn getNextParallelPair Norms =
(
local stop = false
local Pair
for i = 1 to Norms.count - 1 do (
for j = i + 1 to Norms.count do (
if distance Norms[i].dir Norms[2].dir < 0.0001 then (
Pair = #(Norms[i], Norms[j])
deleteItem Norms j
deleteItem Norms i
stop = true
)
if stop then exit
)
if stop then exit
)
Pair
),
fn smoothify =
(
local Norms = copy Vert.OriginalNormals #noMap
local N = undefined
While Norms.count >= 2 do (
N = s_NormalDataLight()
local Pair = getNextParallelPair Norms
if Pair != undefined then (
N.weight = Pair[1].weight + Pair[2].weight
N.pos = ((Pair[1].pos * Pair[1].weight) + (Pair[2].pos * Pair[2].weight)) / N.weight
N.dir = ((Pair[1].dir * Pair[1].weight) + (Pair[2].dir * Pair[2].weight)) / N.weight
) else (
N1 = Norms[1]
N2 = Norms[2]
N.weight = N1.weight + N2.weight
local piv = lineLineIntersect N1.pos N1.dir N2.pos N2.dir
local rad = ((distance piv N1.pos) * N1.weight + (distance piv N2.pos) * N2.weight) / N.weight
local vec = normalize ((N1.pos - piv) * N1.weight + (N2.pos - piv) * N2.weight)
N.pos = (piv + vec * rad)
N.dir = vec
deleteItem Norms 2
deleteItem Norms 1
)
append Norms N
)
if N != undefined then
polyop.setVert owner Vert.index N.pos
),
fn reconstruct =
(
dir = [0,0,0]
for N in Vert.OriginalNormals do
dir += N.dir
dir = normalize dir
owner.norms.setNormal index dir
),
valid = init()
)
struct s_FaceData
(
owner,
index,
center,
MidVert,
originalVerts = #(),
OriginalVertsData = #(),
originalEdges = #(),
OriginalNormals = #(),
EdgesData = #(),
faceChildren = #{},
midEdgeVerts = #(),
MidEdgeVertsData = #(),
fn getFaceNormals face:index =
(
local faceVerts = polyop.getFaceVerts owner face
local OVerts = for i = 1 to originalVerts.count where findItem faceVerts originalVerts[i] > 0 collect OriginalVertsData[i]
local EVerts =for i = 1 to midEdgeVerts.count where findItem faceVerts midEdgeVerts[i] > 0 collect MidEdgeVertsData[i]
local Verts = #()
join Verts OVerts
join Verts EVerts
if MidVert != undefined then
append Verts MidVert
local NormsData = #()
for V in Verts do (
local NewNormData = s_NormalData owner:owner faceIndex:face Vert:V
if NewNormData.valid then
append NormsData NewNormData
)
NormsData
),
fn getNormalByVert v =
(
for N in OriginalNormals where N.Vert.index == v do
exit with N
),
fn init =
(
if not polyop.isFaceDead owner index then (
center = polyop.getFaceCenter owner index
originalVerts = polyop.getFaceVerts owner index
originalEdges = polyop.getFaceEdges owner index
OriginalVertsData = for v in originalVerts collect s_VertData index:v
OriginalNormals = getFaceNormals()
faceChildren[index] = true
for V in OriginalVertsData do
V.OriginalNormals = #(getNormalByVert V.index)
-- format "Face % initialized
" index
true
) else
false
),
fn setEdgesData Edges =
(
EdgesData = for i in originalEdges collect Edges[i]
),
/* Optymized by Denis Trofimov */
fn divide =
(
local numFacesBefore = owner.numfaces
for D in EdgesData do (
local OrigNorms = for v in D.verts collect OriginalNormals[findItem originalVerts v]
append midEdgeVerts D.midVert
append MidEdgeVertsData (s_VertData index:D.midVert OriginalNormals:OrigNorms)
)
local newEdge = polyop.createEdge owner midEdgeVerts[1] midEdgeVerts[2]
MidVert = s_VertData index:(polyop.divideEdge owner newEdge 0.5) OriginalNormals:OriginalNormals
polyop.setVert owner MidVert.index center
for i = 3 to MidEdgeVerts.count do
polyop.createEdge owner midEdgeVerts[i] MidVert.index
local numFacesAfter = owner.numfaces
for i = numFacesBefore + 1 to numFacesAfter do
faceChildren[i] = true
),
fn reconstructNormals =
(
for f in faceChildren do (
local NewFaceNormals = getFaceNormals face:f
for N in NewFaceNormals do
N.reconstruct()
)
-- format "Face % normals reconstructed
" index
),
fn smoothify =
(
for f in faceChildren do (
local NewFaceNormals = getFaceNormals face:f
for N in NewFaceNormals do
N.smoothify()
)
),
valid = init()
)
fn tessellateObject tar =
(
if canConvertTo tar Editable_Poly then (
local ts = timeStamp()
max modify mode
local obj = convertToPoly (copy tar)
obj.transform = matrix3 1
addModifier obj (edit_normals name:"norms")
modPanel.setCurrentObject obj.norms
obj.norms.SelLevel = #Object
local EdgesData = #()
local FacesData = #()
/* Edge Data collection loop */
for i = 1 to polyop.getNumEdges obj do (
local NewEdgeData = s_EdgeData owner:obj index:i
if NewEdgeData.valid then
append EdgesData NewEdgeData
)
/* Face Data collection loop */
for i = 1 to polyop.getNumFaces obj do (
local NewFaceData = s_FaceData owner:obj index:i
if NewFaceData.valid then (
NewFaceData.setEdgesData EdgesData
append FacesData NewFaceData
)
)
/* Edges Subdivision loop */
for D in EdgesData do
D.divide()
/* Faces Subdivision loop */
for F in FacesData do
F.divide()
/* Normals loop */
disableRefMsgs()
for F in FacesData do
F.reconstructNormals()
obj.norms.MakeExplicit()
enableRefMsgs()
/* Smooth new verts loop */
for F in FacesData do
F.smoothify()
collapseStack obj
obj.transform = tar.transform
format "% tessellation took %
" obj.name (timeStamp() - ts)
gc()
obj
) else
undefined
)
local newObjs = #()
for o in selection do (
local newObj = tessellateObject o
if newObj != undefined then
append newObjs newObj
)
select newObjs
redrawViews()
)
Here’s mine:
undo off (
normChan = 7
select $target
max modify mode
addmodifier $target (edit_normals name:"BeforeNorms")
beforeNorms = $target.BeforeNorms
polyop.setMapSupport $target.baseobject normChan true
polyop.setNumMapVerts $target.baseobject normChan (beforeNorms.GetNumNormals())
polyop.setNumMapFaces $target.baseobject normChan (beforeNorms.GetNumFaces())
-- Copy the normals to a UV map channel
for n = 1 to (beforeNorms.GetNumNormals()) do
(
polyop.setMapVert $target.baseobject normChan n (beforeNorms.GetNormal n)
)
for face = 1 to (beforeNorms.GetNumFaces()) do
(
normIds = for corner = 1 to (beforeNorms.GetFaceDegree face) collect
(
(beforeNorms.GetNormalId face corner)
)
polyop.setMapFace $target.baseobject normChan face normIds
)
-- Do the tessellation
addmodifier $target (tessellate faceType:1 Type:1 iterations:1)
addmodifier $target (turn_to_poly())
-- Copy the normals back out, now that tessellation has happened
addmodifier $target (edit_normals name:"AfterNorms")
afterNorms = $target.AfterNorms
afterNorms.MakeExplicit selection:#{1..AfterNorms.GetNumNormals()}
afterVertCount = polyop.getNumMapVerts $target normChan
_polyOp_getMapVert = polyOp.getMapVert
_AfterNorms_SetNormal = AfterNorms.SetNormal
-- Critical that we disable the reference message updates when setting the normals
disableRefMsgs()
for vert = 1 to afterVertCount do
(
-- The interpolated normals will need to be normalized
--norm = normalize (polyOp.getMapVert $target normChan vert)
norm = _polyOp_getMapVert $target normChan vert
_AfterNorms_SetNormal vert &norm
)
afterFaceCount = polyop.getNumMapFaces $target normChan
_polyop_getMapFace = polyop.getMapFace
_AfterNorms_SetNormalId = AfterNorms.SetNormalId
for face = 1 to afterFaceCount do
(
normIds = _polyop_getMapFace $target normChan face
for corner = 1 to normIds.count do
(
_AfterNorms_SetNormalId face corner normIds[corner]
)
)
enableRefMsgs()
-- Rebuild everthing with ref messages restored
afternorms.RebuildNormals()
)
EDIT: Tweaked the tessellation modifier parameters to make it look more interesting
Nice touch, very clever.
EDIT: Sorry, didn’t see it previously in matans/denis code
Hey, I didn’t see it either!
I almost didn’t bother to submit an entry. The thing was so incredibly slow without it, I thought I’d crashed max.
After seeing how similar it was to some of the early posts and reading Denis’ hints I realized I must be missing something… something that was probably the entire point of the challenge.
I was pretty shocked when disabling messaging had such an impact
here is a time to show my version:
fn tesselateKeepNormals node: tension:0 type:0 iterations:0 turbo:off =
(
if node == unsupplied do node = selection[1]
if iskindof node Editable_poly do
(
windows.sendmessage (windows.getmaxhwnd()) 0x000B 0 1
sel = selection as array
clearselection()
mode = getCommandPanelTaskMode()
setCommandPanelTaskMode mode:#modify
select node
NORM_CH = 5
GetMapFace = polyop.getMapFace
GetMapVert = polyop.getMapVert
SetMapVert = polyop.setMapVert
polyop.setMapSupport node NORM_CH on
polyop.defaultMapFaces node NORM_CH
polyOp.applyUVWMap node #face channel:NORM_CH
modpanel.addmodtoselection (norm = edit_normals displayLength:0)
GetFaceDegree = norm.GetFaceDegree
GetNormalID = norm.GetNormalID
GetNormal = norm.GetNormal
for f=1 to norm.GetNumFaces() do
(
corners = GetMapFace node NORM_CH f
for c=1 to (GetFaceDegree f) do
(
id = GetNormalID f c
SetMapVert node NORM_CH corners[c] (GetNormal id)
)
)
if turbo then
(
modpanel.addmodtoselection (turbo = turbosmooth iterations:iterations update:3)
converttopoly node
)
else
(
modpanel.addmodtoselection (tess = tessellate tension:tension type:type iterations:iterations faceType:1)
)
modpanel.addmodtoselection (modi = edit_normals displayLength:0)
GetFaceDegree = modi.GetFaceDegree
GetNormalID = modi.GetNormalID
GetNormal = modi.GetNormal
SetNormal = modi.SetNormal
disableRefMsgs()
for f=1 to modi.GetNumFaces() do
(
verts = GetMapFace node NORM_CH f
for c=1 to (GetFaceDegree f) collect
(
id = GetNormalID f c
SetNormal id (GetMapVert node NORM_CH verts[c])
)
)
modi.MakeExplicit selection:#{1..modi.GetNumNormals()}
enableRefMsgs()
converttopoly node
select sel
windows.sendmessage (windows.getmaxhwnd()) 0x000B 1 1
setCommandPanelTaskMode mode:mode
)
node
)
/*
t1 = timestamp()
tesselateKeepNormals type:0
format "time: %
" (timestamp() - t1)
*/
it's almost identical to biddle's version.
Technically biddle's version has to be faster because he stores normal per normal, but i store per corner. in most my cases i had all normals broken. so it was easy to hold all corners.
my version includes disabling window redraw. it helps the algorithm works faster with no flickering.
second time the winner is [b]biddle[/b]. because as i said his version is algorithmically faster than mine.
pair
[b]disableRefMsgs()[/b]
[b]enableRefMsgs()[/b]
is exactly what i tried to hint to... ;)
[i]edit[/i]: cleaned the code...
this thing is very topical when you make both high and low poly models to bake the normals into texture map. using of explicit normals makes the rendering cleaner than rendering with traditionally used face smoothing groups.
In my real pipeline I have two scripted modifiers which extend the Edit_Normal modifier.
The first one holds the normals in map channel, the second one fetches the normals. It allows me to do any poly editing in between modifiers and see the final result…
i was shocked too. but it was about 3 years ago when the problem occurred the first time for me.
so i was cheating… i’ve asked for the solution that i already had and polished for many years.
i’m sorry
the interesting thing… i’m not sure that storing normals by normal guaranties correct final restoring for any poly editing. i have to double-check it.
as i said it’s faster than store by corner. but storing by corner works well for sure.