Notifications
Clear all

[Closed] Mini-Challenge #2

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.

  1. applies to me, but also
  2. 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…

2 Replies
(@gravey)
Joined: 11 months ago

Posts: 0

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

(@davewortley)
Joined: 11 months ago

Posts: 0

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

 lo1

Nice touch, very clever.

EDIT: Sorry, didn’t see it previously in matans/denis code

1 Reply
(@biddle)
Joined: 11 months ago

Posts: 0

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.

Page 4 / 5