Converting simple renderer script to max sdk plugin


#7

wow thats cool ! can you share your code? what do you mean by caching? saving it to a variable ?
I want to move all objects and lights. What do you mean by caching functions?
i am running win 10 on lenovo w510 i7 q820 1.7ghz quadcore , gpu quadro fx 880m and 16gb ram and ssd drive and am at 90ms on “turbo mode” = /

ps . how did you make that gif? is there a tool for these kind of gifs?


#8

If you want to move everything then caching will be mostly useless
caching functions is something that you already did with GetFaceCenter

I use ShareX for gifs


#9

Hello @Flub,

Could you provide a full description of the task or a test scene?

Amount of objects, faces, lights, animated parameters of any involving node?

I’ll see if I can help you with this.


#10


example_scene01.max (732 KB)

Hello,
I created a test scene which shows the scenario.
in the scene you see a white object . it consists mainly out of planar quad faces. I have a real object with the same structure with lights installed in a way that i am able to control the brightness of every face of it. The goal is to realtime render/calculate how bright every of its faces is in different constellations of objects and light sources. For now only the angle of every face and a drop shadow are implemented in my script.
I want to be able to play around with animations and different objects in max and realtime transfer the look of the object in the max viewpor to the real object via telnet or serial port.
The red objects in the testscene are just examples to see the shadow.
The yellow squares with 6x6 faces are a future idea to be implemented. actually it is just the idea to select one or more objects which faces brightness are calculated and sent out in realtime. Than it would also be perhaps necessary to define which of them throws shadows and which are excluded from this.
Because of your help there is no problem for me to build it with the script now. But it is actually too slow. It takes 200 ms per frame for the test scene at the moment. light_transfer_script_edit09.ms (7.2 KB)

Thank you!!!
Flub


#11

OK, here is a little optimized version. I’ve also included a test scene with animated objects and lights for better comparison.

It can still be optimized.

It runs around 4-5 times faster.

(
	/* SETUP TEST SCENE ####################################################################################################### */
	
	delete objects
	
	master = converttopoly (plane length:300 width:300 pos:[0,0,0] lengthsegs:15 widthsegs:15 name:"master" wirecolor:gray)
	
	max zoomext sel
	
	b1 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[ 50,   0, 50] wirecolor:red)
	b2 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[-50,   0, 50] wirecolor:red)
	b3 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[  0,  50, 50] wirecolor:red)
	b4 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[  0, -50, 50] wirecolor:red)
	
	converttopoly (box lengthsegs:1 widthsegs:1 heightsegs:1 length:20 width:20 height:75 pos:[0, 0, 0] wirecolor:orange)

	l1 = omnilight rgb:(color 255 255 255) pos:[50,0,120] multiplier:1.0
	l2 = omnilight rgb:(color 255 255 255) pos:[ 0,0,150] multiplier:0.0
	
	d1 = dummy()
	d2 = dummy()
	
	b1.parent = b2.parent = b3.parent = b4.parent = d1
	l1.parent = d2
	
	with animate on
	(
		at time 100
		(
			rotate d1 (angleaxis  90 [0,0, 1])
			rotate d2 (angleaxis 360 [0,0,-1])
		)
	)
	
	/* END SETUP TEST SCENE ################################################################################################### */
	
	try destroydialog ::RO_DISPLAY_FACES_COLORS catch()
	
	rollout RO_DISPLAY_FACES_COLORS "Faces Colors" width:172 height:100
	(
		checkbutton bt_start "Start" pos:[8,8] width:154 height:32
		
		spinner sp_l1 "Light 1 Multiplier: " pos:[8,54] fieldwidth:48 range:[0,1,1.0] scale:0.01
		spinner sp_l2 "Light 2 Multiplier: " pos:[8,76] fieldwidth:48 range:[0,1,0.0] scale:0.01
		
		global GW_DisplayFacesColors
		
		local node = $master
		
		local GetfaceNormal = polyop.getfacenormal
		local GetFaceCenter = polyop.getfacecenter
		local GetFaceVerts  = polyop.getfaceverts
		local GetVert       = polyop.getvert
		local SetMapVert    = polyop.setmapvert
		
		local MRIntersect  = #()
		local facesVerts   = #()
		local mapFaces     = #()
		local sourceLights = #()
		local colour       = [0,0,0]
		
		fn CalculateFacesColors = with undo off
		(
			MRIntersect = for j in geometry where j != node collect
			(
				rm = RayMeshGridIntersect()
				rm.Initialize 5
				rm.addNode j 
				rm.buildGrid()
				#(rm, rm.intersectRay)
			)
			
			for f = 1 to node.numfaces do
			(
				value = 0
				
				faceNormal = GetfaceNormal node f
				faceCenter = GetFaceCenter node f
				vertsPos   = for k in facesVerts[f] collect GetVert node k
				
				strength = 1.0/vertsPos.count
				
				for k in sourceLights do
				(
					lightPos = k.center
					lightDir = normalize (lightPos - faceCenter)
					
					shadowStrength = 0
					
					for i in vertsPos do
					(
						for rm in MRIntersect where (rm[2] i (lightPos-i) false) > 0 do shadowStrength += strength
					)
					
					diffuse = amax ((dot lightDir faceNormal)*k.multiplier) 0
					value  += diffuse * (1.0-shadowStrength)
				)
				
				colour.x = colour.y = colour.z = amax 0.0 (amin value 1.0)
				
				for k in mapFaces[f] do SetMapVert node 0 k colour
			)
			
			update node
			
			for rm in MRIntersect do rm[1].free()
		)
		
		fn GW_DisplayFacesColors =
		(
			clearlistener()
			
			st = timestamp(); sh = heapfree
			
			CalculateFacesColors()
			
			format "time:% heap:%\n" (timestamp()-st) (sh-heapfree)
		)
		
		fn SetupScene =
		(
			polyop.applyuvwmap node #face channel:0
			
			sourceLights = for j in lights where classof j != targetobject collect j
			mapFaces     = for j = 1 to node.numfaces collect polyop.getmapface node 0 j
			facesVerts   = for j = 1 to node.numfaces collect GetFaceVerts node j
			
			registerredrawviewscallback GW_DisplayFacesColors
			
			node.vertexColorType  = 0
			node.showVertexColors = on
			
			completeredraw()
		)
		
		on bt_start changed arg do
		(
			unregisterredrawviewscallback GW_DisplayFacesColors
			
			if arg then
			(
				SetupScene()
				playanimation()
				bt_start.text = "Stop"
			)else(
				bt_start.text = "Start"
				stopanimation()
			)
		)
		
		on RO_DISPLAY_FACES_COLORS open do
		(
			unregisterredrawviewscallback GW_DisplayFacesColors
			gc()
		)
		
		on RO_DISPLAY_FACES_COLORS close do unregisterredrawviewscallback GW_DisplayFacesColors
		
		on sp_l1 changed arg do l1.multiplier = arg
		on sp_l2 changed arg do l2.multiplier = arg
		
	)
	
	createdialog RO_DISPLAY_FACES_COLORS

)

#12

What you described looks like build in “Pseudo color exposure control”.
And that you can view in real real time (with “normal” Max lights)
Can view un/smoothed meshes, bump/normal mapped materials also…


#13

Actually, when think bout it, that’s how Max viewport rendering “works” (with lights and shadows).
So don’t get it, you want to transfer that to some specific format, or?


#14

Wow, thats amazing ! thank you thats a big step forward. your testcene runs at 60 ms per frame (around 16 fps) on my computer. Is it much faster on yours?
Do you know if it is an improvement efficient to snapshotToMesh and append all objects instead of the master object to one temporary (data only) object so rm.intersectRay has to run once only? How much speed improvement would a compiled plugin lead to in your opinion? I also just thought about if there s a way to just send out all relevant data on every frame to an external program which is just there to calculate the brightness values out of them. then Max would stuck while running the script. But I think that it would still take some time to output all the mesh data. But i dont know if that s a realistic idea. I mean perhaps it is possible to write a standalone windows program which somehow gets data and calculates but much faster then the script evaluation.

@domos
I want to transfer the appearance of the virtual 3d model to a real one made of plastic and leds.

— another idea is to process the critical calculations via dotnet, do you think that might be a way


#15

The test scene runs at 14-15 ms (66 FPS) per iteration on my end, while the original code runs at 70-71 (14 FPS).

The most critical point I see is to reduce the amount of ray test. Currently, there are 4 test for most of the vertices, when we only need 1.

So for the plane (225 faces) we are casting 900 rays per light, but we only need to cast 256. That will give you a much better performance.

Other point was that SetFaceColor() is much slower than SetMapVert().

I don’t think there will be a huge performance gain using the SDK.

Things that could be improved:

  • Cast only 1 ray per light and vertex
  • If the lit object is static, then caching the values (normal, vertex positions, etc.)
  • If the “blockers” objects (the ones that cast the shadows) are spheres, planes or boxes, then there might be a chance to improve the ray casting by using a custom routine. This should be better to implement using the SDK.
  • If you will just send an array of integers via telnet, then there should be some speed up since you don’t need to actually lit the object.

At the moment I can’t think of any other thing that could have a big impact in the performance.

Here is a different approach, casting only one ray per vertex, but only works with 1 light. It could be modified though to work with more lights.

As you can see it performs around 2 times faster.

(
	/* SETUP TEST SCENE ####################################################################################################### */
	
	delete objects
	
	master = converttopoly (plane length:300 width:300 pos:[0,0,0] lengthsegs:15 widthsegs:15 name:"master" wirecolor:gray)
	
	max zoomext sel
	
	b1 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[ 50,   0, 50] wirecolor:red)
	b2 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[-50,   0, 50] wirecolor:red)
	b3 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[  0,  50, 50] wirecolor:red)
	b4 = converttopoly (box lengthsegs:2 widthsegs:2 heightsegs:2 length:20 width:20 height:20 pos:[  0, -50, 50] wirecolor:red)
	
	converttopoly (box lengthsegs:1 widthsegs:1 heightsegs:1 length:20 width:20 height:75 pos:[0, 0, 0] wirecolor:orange)

	l1 = omnilight rgb:(color 255 255 255) pos:[50,0,110] multiplier:1.0
	l2 = omnilight rgb:(color 255 255 255) pos:[ 0,0,150] multiplier:0.0
	
	d1 = dummy()
	d2 = dummy()
	
	b1.parent = b2.parent = b3.parent = b4.parent = d1
	l1.parent = d2
	
	with animate on
	(
		at time 100
		(
			rotate d1 (angleaxis  90 [0,0, 1])
			rotate d2 (angleaxis 360 [0,0,-1])
		)
	)
	
	/* END SETUP TEST SCENE ################################################################################################### */
	
	try destroydialog ::RO_DISPLAY_FACES_COLORS catch()
	
	rollout RO_DISPLAY_FACES_COLORS "Faces Colors" width:172 height:100
	(
		checkbutton bt_start "Start" pos:[8,8] width:154 height:32
		
		spinner sp_l1 "Light 1 Multiplier: " pos:[8,54] fieldwidth:48 range:[0,1,1.0] scale:0.01
		spinner sp_l2 "Light 2 Multiplier: " pos:[8,76] fieldwidth:48 range:[0,1,0.0] scale:0.01 enabled:false
		
		global GW_DisplayFacesColors
		
		local node = $master
		
		local GetfaceNormal = polyop.getfacenormal
		local GetFaceCenter = polyop.getfacecenter
		local GetFaceVerts  = polyop.getfaceverts
		local GetVert       = polyop.getvert
		local SetMapVert    = polyop.setmapvert
		
		local MRIntersect  = #()
		local facesVerts   = #()
		local mapFaces     = #()
		local sourceLights = #()
		local colour       = [0,0,0]
		
		fn CalculateFacesColors = with undo off
		(
			MRIntersect = for j in geometry where j != node collect
			(
				rm = RayMeshGridIntersect()
				rm.Initialize 5
				rm.addNode j
				rm.buildGrid()
				#(rm, rm.intersectRay)
			)
			
			vertsHits = #()
			for j = 1 to node.numverts do
			(
				vpos = GetVert node j
				hits = 0
				
				for k in sourceLights do
				(
					lightPos = k.center
					for rm in MRIntersect where (rm[2] vpos (lightPos-vpos) false) > 0 do hits += 1
				)
				vertsHits[j] = hits
			)
			
			for f = 1 to node.numfaces do
			(
				value = 0
				
				faceNormal = GetfaceNormal node f
				faceCenter = GetFaceCenter node f
				vertsPos   = for k in facesVerts[f] collect GetVert node k
				
				strength = 1.0/vertsPos.count
				
				for k in sourceLights do
				(
					lightPos = k.center
					lightDir = normalize (lightPos - faceCenter)
					
					shadowStrength = 0
					
					for i in facesVerts[f] do
					(
						for v = 1 to vertsHits[i] do shadowStrength += strength
					)
					
					diffuse = amax ((dot lightDir faceNormal)*k.multiplier) 0
					value  += diffuse * (1.0-shadowStrength)
				)
				
				colour.x = colour.y = colour.z = amax 0.0 (amin value 1.0)
				
				for k in mapFaces[f] do SetMapVert node 0 k colour
			)
			
			update node
			
			for rm in MRIntersect do rm[1].free()	-- Prevent memory leaking
		)
		
		fn GW_DisplayFacesColors =
		(
			clearlistener()
			
			st = timestamp(); sh = heapfree
			
			CalculateFacesColors()
			
			format "time:% heap:%\n" (timestamp()-st) (sh-heapfree)
		)
		
		fn SetupScene =
		(
			polyop.applyuvwmap node #face channel:0
			
			sourceLights = for j in lights where classof j != targetobject collect j
			mapFaces     = for j = 1 to node.numfaces collect polyop.getmapface node 0 j
			facesVerts   = for j = 1 to node.numfaces collect GetFaceVerts node j
			
			registerredrawviewscallback GW_DisplayFacesColors
			
			node.vertexColorType  = 0
			node.showVertexColors = on
			
			completeredraw()
		)
		
		on bt_start changed arg do
		(
			unregisterredrawviewscallback GW_DisplayFacesColors
			
			if arg then
			(
				SetupScene()
				playanimation()
				bt_start.text = "Stop"
			)else(
				bt_start.text = "Start"
				stopanimation()
			)
		)
		
		on RO_DISPLAY_FACES_COLORS open do
		(
			unregisterredrawviewscallback GW_DisplayFacesColors
			gc()
		)
		
		on RO_DISPLAY_FACES_COLORS close do unregisterredrawviewscallback GW_DisplayFacesColors
		
		on sp_l1 changed arg do l1.multiplier = arg
		on sp_l2 changed arg do l2.multiplier = arg
		
	)
	
	createdialog RO_DISPLAY_FACES_COLORS

)

This code with just 1 light, runs at 8 ms per iteration (125 FPS).


#16

so what about VertexPaint modifier, did you try it?
standard 4 segment Teapot runs at about 10fps with 2 lights and shadows on
(but it feels like it has some issues with self-shadowing)

(
		
	modPanelHWND = undefined
	for c in UIAccessor.GetChildWindows (windows.getMAXHWND()) while modPanelHWND == undefined where (d = windows.getHWNDData c)[4] == "ModifyTask" do modPanelHWND = d[1]

	for c in windows.getChildrenHWND modPanelHWND where c[4] == "CustButton" and c[5] == "Assign" do
	(
		hwnd = c[1]
		for i=1 to 30 do
		(
			slidertime = i		
			UIAccessor.PressButton hwnd
		)
		
		exit
	)

)

tcPHoJAT9H


#17

@PolyTools3D
Is your GUI also flickering while running the script?


@Serejah
is there something missing in your code, i have to think about it i didnt get what your script is doing yet


#18

Yes, the Set Keys button flickers.

BTW, I don’t think we are working with the right approach for this. If the task is just to get the color of the faces there must be a much better way of doing it.

You should be able to use much more dense geometry and a lot more lights and get it working at 120 FPS (at least).


#19

I think so too …


#20

Yes, I think so too but I didnt find it so far.

I started with the approach of render to texture onto a very small uv map in real time, it was not perfomative and bad for further user interactions because max cannot do it without selecting the object to be baked.

Then you pointed out the idea of capturing the Viewport from left and right. (thats possible with my model) which was quite good in performance but jitttery in drop shadow values, so the result was flickering kind of when shadows moves in + the UX suffers alot because the gui is stuttering while captures are being made + two viewports are unusable.

The third approach (based on the svg renderer script) runs best so far. But as you said it seems there must be a more efficent approach to get a better realtime experience.

I dont know if i should just use a different software for it, but i dont know which one is structured in a way you can grab internal rendering results from the viewport preview renderer.

I am thinking about if OSL Shaders might offer some possiblities concerning this. But I think actually you cannot get the face id and light positions from the scene and you cannot output things via telnet … but via viewport capturing a small rectangle filled with the resulting data one could get back to max script from the shader.

Is 3ds Max Interactive helpful ? i have no clue if it is for anything other than VR interactions.


#21

For the previous task, doing it in MXS was a good decision I guess. It didn’t take a lot of work and it did the job.

Now for casting shadows it is a different thing and MXS isn’t good at ray casting.

So, I think if you want a more robust solution, you could do some custom little render engine (very minimal) in .Net and multithreading to avoid doing it in SDK and C++ as the performance will not be huge but the work will be definitely harder.

Other than that, if you wanted to work in C++ or Java, I would just forget Max and move to a tool using OpenGL or Vulkan, I don’t know which one would be better, but they both would outperform Max.

Perhaps there is a solution using Max and shaders, but I can’t think of any. Would love to know what more experienced developers can suggest.


#22

Okay. I am just reading through what .net is and starting to do tutorials. Do you know what kind of app has to be written and how it will be accessed from within max? I think i am gonna learn c++ .net programming as i know c stuff from µC programming a little.

Or do I have to use C# i just started this : https://www.youtube.com/watch?v=1CgsMtUmVgs please tell me if it is the totally wrong direction.
I tried out this but max 2019 does not have utitlities->maxdotnet option
https://www.youtube.com/watch?v=MqyrBop5uzc

The Problem with all these other programs is that they actually dont have the interface to play with different kind of standard objects (text, teapot, vectorgraphics), light and animation, creating loops etc like max has. I started to look into blenders sourcecode to see if i can find the place where viewport is rendered but it is too complicated to understand for me at them moment. and anyways i dont like the kind of navigation in space blender offers. (perhaps i am just used to max)


#23

Okay, if I understand that correctly, I want to write a .net assembly. which is actually some C# code which contains a class with public methods or so. After compiling it to a dll. I copy that dll somewhere in the 3ds max 2019 directory structure. After this I can somehow access those methods from within Maxscript. In other words i can pass data to these functions and retrieve data. All in all i could profit from the better performance of compiled code.
Is this right? If so, can someone help me finding a point to start from? are there any Tutorials or examples of someone who did a similar way of extending .net functions for use in max script? I am very confused by all these different versions of visual studio, as I could only get the sdk plugin wizard running on visual studio express 2015 because in newer versions the file structure changed and i didnt knew where to add those wizard files (vsz …)
Anyways I think I ve understood that, the idea of using a .net thing (assembly?) means it is not related to max sdk anymore.


#24

I doubt that you can benefit from it at all unless you’re willing to implement anything similar to RayMeshGridIntersect.
At least my attempts to use mesh.IntersectRay without any accelerating structure didn’t show up any significant perfomance gains compared to mxs RayMeshGridIntersect


#25

@Flub

As we all well understand, this is a trivial task for current rendering engines and algorithms. The only problem is how and in what format you want to get the result.

And you still have not told us about this.


#26

I don’t know how yet but I just want to have an array with 8bit brightness values (0-255) for each face of an object in real-time To send it serially or via Telnet to some electronics. So there is a real-time relation between the appearance of an object in the viewport and a real object. Just for experiments of perception.
EDIT: I just found this tutorial and I think it is quite interesting for me and other beginners who want to try around extending max script :http://www.klaasnienhuis.nl/2014/08/fly-assemblies-maxscript/