Converting simple renderer script to max sdk plugin


#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/


#27

I just started to follow the tutorial to try around with on the fly assemblies.

(
clearListener()
function fn_onTheFlyAssembly_readFileOps =
(
    /*<FUNCTION>
    Description
        creates an on the fly assembly
        the method reads bytes from a binary file and returns integers. Also takes care of
        converting the data from big endian to little endian
        major parts of this code by denisT: http://forums.cgsociety.org/showpost.php?p=7838323&postcount=6
    Arguments
    Return
    <FUNCTION>*/
    
    --the C# code
    source  = ""
    source += "using System;\n"
	source += "using Autodesk.Max;\n"
    source += "public class ReadFileOps\n"
    source += "{\n"
	source += "    public string helloWorld()\n"
    source += "    {\n"
	source += "        string result = \"hello World\";\n"
	source += "        return result;\n"
    source += "    }\n"
    source += "}\n"

    --setting up the assembly in memory
    csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
    compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
    compilerParams.GenerateInMemory = on
    compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
    compilerResults.CompiledAssembly.CreateInstance "ReadFileOps"
)
local ReadFileOps = fn_onTheFlyAssembly_readFileOps()
helloWorld = ReadFileOps.helloWorld()

)

How could I pass Max geometry and light (or even all scene )data to the dotnet c# function or How could i access geometry from within the function.
And can someone clarify what dotnet actually is, why cant i just use c#?
Is it only used to compile the c# sharp code into memory? if so, when i create a class library dll file instead. Can I access its functions through maxscrip only via dotnet functions? And to get rid of dotnet one has to embed the code for the dll into some code structure which is fitting the plugin api of 3ds max ? But then i cannot access it through max script anymore. How does the viewport renderer functions? and do you think this might be a starting point : http://docs.autodesk.com/3DSMAX/16/ENU/3ds-Max-SDK-Programmer-Guide/index.html?url=files/GUID-40ED5D02-BBCF-4EE3-9EE7-E59425B49CBB.htm,topicNumber=d30e2631

sorry for asking all these questions… Id really love to come closer to the goal.


#28

you are talking about real-time meaning a ‘quick’ way to pass data to the external application. You try to make a ‘quick render’ in the MAX but you have a lot of other things that can slowdown all pipeline - write to file, read from file, parse the data… and these things might be much more critical than the render itself.


#29

I only want output the array from the ram serially or via telnet to some hardware, no file interactions are involved.
I think the actual calculation takes 80 ms per frame on my computer for polyTools3d’s second script with 3ds max 2020. This is without sending the data to hardware but with making the result visible by setting the vertex color.
In some places it says that a plugin code could execute 1000 times faster than using maxscript. But somehow it seems very chaotic and unclear to me how to learn it. Perhaps I should just try to add the rest of the performance fixes to the script and see how fast it runs. But it would be better to have some code which runs like much faster than 35ms per frame so there is some headroom in terms of performance to increase the number of objects to be analysed and to have some time for sending stuff.


#30

Check out this project if you need examples.
But what is your plan? How do you suppose to make script faster using c# compiled dll?


#31

My plan is not a real plan = / I just thought that the compiled version of the calculateFaces function would run faster because it runs on a lower abstraction level than maxscript and uses the cpu more efficient.


#32

Ok, here’s the modified Jorge’s version that uses c# function to speed up setting vertex colors

(
	/* SETUP TEST SCENE ####################################################################################################### */
	
	delete objects
	
	master = convertToMesh (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:50 width:50 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 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
			)

			hits = dotnet.ValueToDotNetObject vertsHits (dotNetClass "system.int32[]")
			lights_n_mults = #()
			for l in lights do
			(
				append lights_n_mults l.pos.x
				append lights_n_mults l.pos.y
				append lights_n_mults l.pos.z
				append lights_n_mults l.multiplier
			)
			lights_n_mults = dotnet.ValueToDotNetObject lights_n_mults (dotNetClass "system.single[]")
			
			SetMeshFaceLigthness node.inode.handle hits lights_n_mults
			
			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 =
		(
			meshop.setMapSupport node 0 true
			
			sourceLights = for j in lights where classof j != targetobject collect 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

)

and c# code

static public void SetMeshFaceLigthness( uint handle, int[] vert_hits, float[] light_pos_and_multipliers )
{

	IGlobal ip = GlobalInterface.Instance;
	IINode node = ip.COREInterface7.GetINodeByHandle( handle );

	if ( node == null ) return;

	//var TM_src = node.GetObjectTM( ip.COREInterface7.Time, null );
	//TM_src.Invert();

	IObject obj = node.EvalWorldState( ip.COREInterface7.Time, true ).Obj;
	node.Dispose();
	ITriObject tri = (ITriObject)obj;
	IMesh mesh = tri.Mesh;
	tri.Dispose();
	obj.Dispose();



	IList<ITVFace> map_faces = mesh.MapFaces( 0 );
	var map_verts = mesh.MapVerts( 0 );

	for ( int f = 0; f < mesh.NumFaces; f++ )
	{
		double value = 0f;


		var meshfaceverts = mesh.Faces[ f ].V;

		IPoint3 N = mesh.FaceNormal( (uint)f, true );
		IPoint3 C = mesh.FaceCenter( (uint)f );

		double strength = 1.0 / 3;


		for ( int i = 0; i < light_pos_and_multipliers.Length; i += 4 )
		{
			//IPoint3 light_pos = ip.Point3.Create( light_pos_and_multipliers[ i ], light_pos_and_multipliers[ i + 1 ], light_pos_and_multipliers[ i + 2 ] );
			//IPoint3 light_dir = light_pos.Subtract( C ).FNormalize;

			double x = light_pos_and_multipliers[ i ] - C.X;
			double y = light_pos_and_multipliers[ i + 1 ] - C.Y;
			double z = light_pos_and_multipliers[ i + 2 ] - C.Z;
			double len = Math.Sqrt( x * x + y * y + z * z );

			x = x / len;
			y = y / len;
			z = z / len;

			double shadowStrength = 0;

			for ( int v = 0; v < 3; v++ )
			{

				for ( int j = 0; j < vert_hits[ meshfaceverts[ v ] ]; j++ )
				{
					shadowStrength += strength;
				}

			}
			
			//double diffuse = Math.Max( (light_dir.X * N.X + light_dir.Y * N.Y + light_dir.Z * N.Z) * light_pos_and_multipliers[ i + 3 ], 0 );
			double diffuse = Math.Max( (x * N.X + y * N.Y + z * N.Z) * light_pos_and_multipliers[ i + 3 ], 0 );

			value += diffuse * (1.0 - shadowStrength);

		}

		float avg_value = (float)(Math.Max( Math.Min( value, 1 ), 0 ));


		map_verts[ (int)map_faces[ f ].T[ 0 ] ].Set( avg_value, avg_value, avg_value );
		map_verts[ (int)map_faces[ f ].T[ 1 ] ].Set( avg_value, avg_value, avg_value );
		map_verts[ (int)map_faces[ f ].T[ 2 ] ].Set( avg_value, avg_value, avg_value );

		N.Dispose();
		C.Dispose();
	}

	mesh.Dispose();
	

}

it can further be heavily optimized using unsafe for direct memory reads/writes
and don’t forget to dispose everything that needs to be


#33

Wow, thank you very much for your help! I am happy to check it out as soon as possible. The setting of the vertex colors inside max is actually only kind of a monitoring for the generated data which is good for debugging and nice to have but not the main goal as data will be displayed on a real object with leds behind each polygon. But I see that the c# code has all except the vertHits calculation inside already.
thanks again, i hope i can get it running, as i dont know yet what to do with the c# code. Do I have to compile it as .dll and put it in max root directory to access it from script? I didnt find any dll loading code in your maxscript.
Thanks again!
Flub


#34

I use visual studio so for me it is easier to load it like this, but you can compile it on-the-fly right from the maxscript

(
  dllpath = @"C:\...\visual studio 2015\Projects\test\bin\Release\test.dll"
  assembly = (dotnetclass "System.Reflection.Assembly").Load ((dotnetclass "System.IO.File").ReadAllBytes dllpath)
  global mshops = (dotNetClass "System.Activator").CreateInstance (assembly.GetType("test.meshops"))
)

-- and then in maxscript call c# function like this
mshops.GetMeshTotalArea obj.inode.handle

-- or assign c# function to a global variable
GetMeshTotalArea = mshops.GetMeshTotalArea

#35

Okay thats an option. I also have visual studio where i could create a dll. So how did you access it? because there is not dll path or something in your max script.


#36

example


#37

Ok, here is a last pure MXS attempt.

If I understood correctly, your setup is: 2 objects (188 verts each), 1 lite 1 matte and 2 light sources.

This runs at 3-4ms per iteration on my end, which is about 250-280 FPS, width very low memory footprint.

I get the same performance using IntersectRay() (to avoid the UI flickering), but there seems to be a bug in Max 2019 and the faces don’t get lit correctly. However in Max 2016 it works well.

(
	/* SETUP TEST SCENE ####################################################################################################### */
	
	gc()
	delete objects
	
	res = 11
	
	t1 = converttopoly(torus smooth:2 segs:(2*res) sides:res radius1:60 radius2:20 pos:[0,0,0] wirecolor:black name:"lit")
	rotate t1 (angleaxis 90 [0,1,0])
	
	t2 = converttopoly(torus smooth:2 segs:(2*res) sides:res radius1:60 radius2:20 pos:[-100,0,0] wirecolor:black name:"matte")
	rotate t2 (angleaxis 90 [0,1,0])
	
	l1 = omnilight rgb:(color 255 255 255) pos:[-200,0,0] multiplier:1.0 name:"light1"
	l2 = omnilight rgb:(color 255 255 255) pos:[ 200,0,0] multiplier:0.5 name:"light2"
	
	d1 = dummy()
	d2 = dummy()
	
	t2.parent = d1
	l1.parent = l2.parent = d2
	
	animationrange = (interval 0f 200f)
	
	with animate on
	(
		at time 200
		(
			rotate d1 (angleaxis 360 [0,0,1])
			rotate d2 (angleaxis 360 [0,1,0])
		)
	)
	
	/* 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,10,1.0] scale:0.01
		spinner sp_l2 "Light 2 Multiplier: " pos:[8,76] fieldwidth:48 range:[0,10,0.5] scale:0.01
		
		global RedrawCallback
		
		local node    = $lit
		local matte   = $matte
		local light_1 = $light1
		local light_2 = $light2
		
		local GetfaceNormal = polyop.getfacenormal
		local GetFaceCenter = polyop.getfacecenter
		local GetFaceVerts  = polyop.getfaceverts
		local GetVert       = polyop.getvert
		local GetMapFace    = polyop.getmapface
		local SetMapVert    = polyop.setmapvert
		
		local rm                  = RayMeshGridIntersect()
		local rm_Initialize       = rm.Initialize
		local rm_AddNode          = rm.addNode
		local rm_BuildGrid        = rm.buildGrid
		local rm_Free             = rm.free
		local rm_IntersectSegment = rm.intersectSegment
		
		local facesVerts  = #()
		local mapFaces    = #()
		local vertsHits   = #()
		local valuesArray = #()		-- Store the light values for each face (range 0.0-1.0)
		local colour      = [0,0,0]
		
		fn CalculateFacesColors = with undo off
		(
			rm_Initialize 5
			rm_AddNode matte
			rm_BuildGrid()
			
			light_1_pos        = light_1.center
			light_1_multiplier = light_1.multiplier
			
			light_2_pos        = light_2.center
			light_2_multiplier = light_2.multiplier
			
			mesh = snapshotasmesh node
			
			for j = 1 to node.numverts do
			(
				vnormal = GetNormal mesh j
				vpos    = GetVert node j
				
				dir_1 = normalize (light_1_pos - vpos)
				dir_2 = normalize (light_2_pos - vpos)
				
				if (dot dir_1 vnormal) >= 0 do vertsHits[j][1] = (rm_IntersectSegment vpos light_1_pos false) > 0
				if (dot dir_2 vnormal) >= 0 do vertsHits[j][2] = (rm_IntersectSegment vpos light_2_pos false) > 0
			)
			
			free mesh
			
			for f = 1 to node.numfaces do
			(
				faceNormal = GetfaceNormal node f
				faceCenter = GetFaceCenter node f
				
				dir_1 = normalize (light_1_pos - faceCenter)
				dir_2 = normalize (light_2_pos - faceCenter)
				
				ang_1 = dot dir_1 faceNormal
				ang_2 = dot dir_2 faceNormal
				
				value    = 0.0
				strength = 1.0/facesVerts[f].count	-- If all faces have 4 vertex this value is fixed 0.25
				
				illum = 0.0
				if ang_1 >= 0 do
				(
					for i in facesVerts[f] where not vertsHits[i][1] do illum += strength
					value += (amax (ang_1*light_1_multiplier) 0) * illum
				)
				
				illum = 0.0
				if ang_2 >= 0 do
				(
					for i in facesVerts[f] where not vertsHits[i][2] do illum += strength
					value += (amax (ang_2*light_2_multiplier) 0) * illum
				)
				
				value = amax 0.0 (amin value 1.0)
				
				valuesArray[f] = value
				
				colour.x = colour.y = colour.z = value
				
				for k in mapFaces[f] do SetMapVert node 0 k colour
			)
			
			update node
			
			rm_Free()	-- Prevent memory leaking
		)
		
		fn RedrawCallback =
		(
			clearlistener(); st = timestamp(); sh = heapfree
			
			CalculateFacesColors()
			
			format "time:% heap:%\n" (timestamp()-st) (sh-heapfree)
		)
		
		fn SetupScene =
		(
			polyop.applyuvwmap node #face channel:0
			
			mapFaces   = for j = 1 to node.numfaces collect GetMapFace node 0 j
			facesVerts = for j = 1 to node.numfaces collect GetFaceVerts node j
			vertsHits  = for j = 1 to node.numverts collect #(false, false)
			
			registerredrawviewscallback RedrawCallback
			
			node.vertexColorType  = 0
			node.showVertexColors = on
			
			completeredraw()
		)
		
		on bt_start changed arg do
		(
			unregisterredrawviewscallback RedrawCallback
			
			if arg then
			(
				SetupScene()
				playanimation()
				bt_start.text = "Stop"
			)else(
				bt_start.text = "Start"
				stopanimation()
			)
		)
		
		on RO_DISPLAY_FACES_COLORS open do
		(
			unregisterredrawviewscallback RedrawCallback
			gc()
		)
		
		on RO_DISPLAY_FACES_COLORS close do unregisterredrawviewscallback RedrawCallback
		
		on sp_l1 changed arg do
		(
			light_1.multiplier = arg
			redrawviews()
		)
		
		on sp_l2 changed arg do
		(
			light_2.multiplier = arg
			redrawviews()
		)
		
	)
	
	createdialog RO_DISPLAY_FACES_COLORS

)

#38

Thank you very much! Okay i need some time to check out your versions now.

If I understood correctly, your setup is: 2 objects (188 verts each), 1 lite 1 matte and 2 light sources.

Actually my model has 188 planar quad faces and 237 vertices. the other objects are static. The the goal is to be able to play around with creation and positioning of light sources and different objects, and animations in real time. A standard scenario would be: You open the .max file with the master model and start the script. Then you orbit around the model and the script sends out the different brightness values (0-255) which change while orbitting because the default viewports lights are shining on the model and theyre connected to the viewport camera. Then you create a light source to change from the default ligth situation to specific one. you move it around to see how it looks. Then you animate the master model to fall down onto a plane and you see on the real model how the light on the virtual 3d object is changing while the static real object is displaying the light situation on that model. then you create some objects to throw shadows on the master object to see how that looks. Then you arrange or animate them. Then you create another light source which is animated like a sun moving around in a far distance and then you see how that looks on the real object which displays the data.

The next step is actually easily done when the first performs well i think. It would be to add another lowpoly master object and send out the brightness values of its faces also. Then there would be two real objects with leds which brightnesses are controlled by a virtual light scene. If you know what I mean.

sorry for the text getting so long but I hope that explains better what Id like to achieve.


#39

I have modified the way you where calculating the shadows intensity. I think now it is working properly but you still need to test it and see if it is correct.

Also added the array to store the lighting values.


#40

So, all the objects will be dynamic (animated or not), including the lights right?

Well, you need to do all the tests considering the worst case scenario (amount of models, faces, lights, animations, etc.) at least.

If possible make it 50-100% worst and try to be happy with the results. If you are not happy you will probably need to find a different approach to be on a “safe” side.