Converting simple renderer script to max sdk plugin


#41

Hey there,
sorry for the late response, I had some problems with my laptop and so on.
It seems that it is fast enough now, I worked on the script by @PolyTools3D and I can get around 16fps in the example scene with two 244 faces torus objects and 2 omni lightsources. I am also trying to implement the dotnet (dll assembly) of @Serejah now to make monitoring even faster and to get a better understanding of how to work with dotnet in conjunction with maxscript / 3ds Max. The function to display faces colors is actually for meshes and uses ITriObject class. I will try to change it i IPolyObject as am focusing on quads.
rtViewportRender.cs (3.2 KB)
e.g. Line 28-29 (the filename is not right, because there is no rendering code yet)
Now, as I am finetuning the script for my needs I am facing some problems. I think that I am making some mistakes in the rendering calculation. I see a difference in color which I cannot understand between the images of viewport renderer and the monitoring of the script render results. My calculation per light source is
value += (theFillColor * lightColor(range 0-1) * dotLightDirFaceNormal(or diffusecolor) + lightColor * theSpecular) * illumination (depending on dropshadow) * lightmultiplier
But there are these strange darker faces. I thought they have to do with my calculation of dropShadows because i now also check for intersections between the object itself i.e. self shadowing is enabled.


This is the script:polytools3d_04_edit11.ms (8.8 KB)

Do you see where does differences are coming from? And I wondered I ve got another issue which is quite small but perhaps it is easy for you. I ve implemented the switching between monitoring and non monitoring mode using completeredraw() but this stops a playing animation. I already tried to save the animation play state beforehand and check if in the line after the redraw to decide if it is necessary to call playanimation() again, but it seems that completeredraw is somekind of asynchronous because it does not work like this.
Thank you for helping me so much.
Flub


#42

If you want to turn the monitoring on/off without stopping the animation you can check if the animation is playing and only call completeredraw() if it is not playing.

if not (isanimplaying()) do completeredraw()

If the animation is playing, forcing a complete redraw won’t have any effect as everything will be redrawn on next frame anyway.

Regarding the illumination, as you have modified both the script and the scene substantially, it is to be expected a drop in performance as well as the introduction of new bugs.

Please consider what I mentioned earlier, you must have a clear understanding of what the scene will be and what you need to do with it. If you keep modifying this over the time, you might get to a point where you may realize this is not the best approach for your project. I am not saying it is not, but as the “conditions” are unclear, it is a possibility.

Any attempt to code a solution, needless to say to profile it, will most probably be a waste of time if the objective is unclear.

A scene with 2000 triangles with two lights and shadows is something that a very basic render engine should run in you cellphone without any issue, at very high frame rates.


#43

I tried that, but it does not work.


Using completeredraw() anytime the monitoring checkbutton is touched it works, but animations stop.
I think something else also gets reloaded when using completeredraw instead of the regular redraw. Perhaps it has something to do with: the SetupScene function where it says

polyop.applyuvwmap node #face channel:0

Anyways. More important is the correct illumination. Yeah I understand that this, actually I did not change the testscene I just tried to add some controls and features. It is still performing fast enough . (around 15ms)
But as I did not change the basics of the Rendering calculation. I wonder what I am missing in there, so the result is almost equivalent to the viewport renderers result. I am thinking about if it is connected to exposure control or gamma correction. Or if theres a mistake with selfshadowing as I added the object itself to the raymeshgridintersect object. Perhaps I should put the starting point of the ray which is checked for intersections with the object itself, alittle bit outside the object instead of on it, but on the other hand I think the rays which are pointing towards the same direction as the targets object face cannot intersect if the third parameter of the intersectSegment method is set to false:

     rm.intersectSegment vpos lightPos false

I think the approach is good and working, I just want to reach some little goals:
– smaller difference between monitoring result and viewport rendering result
– finding out the type and position of the default light sources of a the viewport if there is no user added light in the scene. I think it is two lights with different multipliers linked to the View position. But i dont know how to find out the exactly same position.
– when that works I want to test things and add utility functions like saving the results for every frame in an animation into a file.
– when its not working, i try to find out if there is way with the dotnet assembly functions to put all the data into it and calculate everything in there. With the way serejah taught me.


#44

I see wat you mean. There seems to be some differences between Max versions. While in Max 2016 you can turn the preview on/off works while playing an animation, in Max 2019 (and I guess others too), switching the vertex preview seems to break the visibility of the node. I don’t know why but someone may have some suggestions on how to work around this issue.

I’ve tried to implement the self-shadowing for the lit object as well as adding something to fix the preview in Max 2019, although it doesn’t work properly.

Here you have some more code to play with.

(
	/* SETUP TEST SCENE ####################################################################################################### */
	
	gc()
	delete objects
	
	res = 11
	
	mtl1 = standard diffusecolor:white selfillumamount:100
	mtl2 = standard diffusecolor:black
	
	t1 = converttopoly(torus smooth:2 segs:(2*res) sides:res radius1:60 radius2:20 pos:[0,0,0] wirecolor:white material:mtl1 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 material:mtl2 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" castShadows:on
	l2 = omnilight rgb:(color 255 255 255) pos:[ 200,0,0] multiplier:0.5 name:"light2" castShadows:on
	
	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_TINY_RENDER catch()
	
	rollout RO_TINY_RENDER "Tiny Render" width:172 height:168
	(
		checkbutton bt_enable       "Enable"       pos:[ 8, 8] width:154 height:32
		checkbox    chk_preview     "Preview"      pos:[ 8,50] checked:true
		checkbox    chk_shadows     "Shadows"      pos:[ 8,68] checked:true
		checkbox    chk_selfshadows "Self Shadows" pos:[80,68] checked:true
		
		spinner sp_l1 "Light 1 Multiplier: " pos:[8, 94] fieldwidth:48 range:[0,10,1.0] scale:0.01
		spinner sp_l2 "Light 2 Multiplier: " pos:[8,114] fieldwidth:48 range:[0,10,0.5] scale:0.01
		
		label lb_statistics "" pos:[8,148] align:#left
		
		global RedrawCallback
		
		local DELTA = 0.00001
		
		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 rm1                  = RayMeshGridIntersect()
		local rm1_Initialize       = rm1.Initialize
		local rm1_AddNode          = rm1.addNode
		local rm1_BuildGrid        = rm1.buildGrid
		local rm1_Free             = rm1.free
		local rm1_IntersectSegment = rm1.intersectSegment
		
		local rm2                  = RayMeshGridIntersect()
		local rm2_Initialize       = rm2.Initialize
		local rm2_AddNode          = rm2.addNode
		local rm2_BuildGrid        = rm2.buildGrid
		local rm2_Free             = rm2.free
		local rm2_IntersectSegment = rm2.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]
		local statistics  = stringstream ""
		
		local preview     = true
		local shadows     = true
		local selfShadows = true
		
		fn RenderFaces = with undo off
		(
			rm1_Initialize 5
			rm1_AddNode matte
			rm1_BuildGrid()
			
			rm2_Initialize 5
			rm2_AddNode node
			rm2_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
				(
					vpos_1 = vpos + (dir_1*DELTA)
					hit = (rm1_IntersectSegment vpos_1 light_1_pos false) > 0
					
					if selfShadows and not hit do hit = (rm2_IntersectSegment vpos_1 light_1_pos false) > 0
					vertsHits[j][1] = hit
				)
				
				if (dot dir_2 vnormal) >= 0 do
				(
					vpos_2 = vpos + (dir_2*DELTA)
					hit = (rm1_IntersectSegment vpos_2 light_2_pos false) > 0
					
					if selfShadows and not hit do hit = (rm2_IntersectSegment vpos_2 light_2_pos false) > 0
					vertsHits[j][2] = hit
				)
			)
			
			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
				
				if preview == true do
				(
					colour.x = colour.y = colour.z = value
					for k in mapFaces[f] do SetMapVert node 0 k colour
				)
			)
			
			update node
			
			rm1_Free()	-- Prevent memory leaking
			rm2_Free()	-- Prevent memory leaking
		)
		
		fn RenderFacesNoShadows = with undo off
		(
			light_1_pos        = light_1.center
			light_1_multiplier = light_1.multiplier
			
			light_2_pos        = light_2.center
			light_2_multiplier = light_2.multiplier
			
			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  = amax (ang_1*light_1_multiplier) 0
				value += amax (ang_2*light_2_multiplier) 0
				
				value = amax 0.0 (amin value 1.0)
				
				valuesArray[f] = value
				
				if preview == true do
				(
					colour.x = colour.y = colour.z = value
					for k in mapFaces[f] do SetMapVert node 0 k colour
				)
			)
			
			update node
		)
		
		local elapsedTime    = 0
		local elapsedCounter = 0
		
		fn RedrawCallback =
		(
			st = timestamp(); sh = heapfree
			
			if shadows == true then RenderFaces() else RenderFacesNoShadows()
			
			elapsedTime    += timestamp()-st
			elapsedCounter += 1
			
			if elapsedCounter >= 60 do
			(
				elapsedTime /= 60
				fps = int ((1000.0/elapsedTime) + 0.5)
				
				free statistics
				format "% FPS  |  %ms  |  heap:%\n" fps elapsedTime (sh-heapfree) to:statistics
				
				lb_statistics.text = statistics
				
				elapsedTime    = 0
				elapsedCounter = 0
			)
		)
		
		fn RedrawViewports =
		(
			for j = 1 to 2 do
			(
				if not (isanimplaying()) then completeredraw() else redrawviews()
			)
		)
		
		fn SetupScene =
		(
			polyop.applyuvwmap node #face channel:0
			
			for j = 1 to polyop.getnummapverts node 0 do SetMapVert node 0 j [0,0,0]
			for j = 1 to node.numfaces do polyop.setfacesmoothgroup node j 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   = preview
			node.vertexColorsShaded = on
			
			RedrawViewports()
		)
		
		on bt_enable changed arg do
		(
			unregisterredrawviewscallback RedrawCallback
			
			for j in RO_TINY_RENDER.controls where j != bt_enable do j.enabled = arg
			
			if arg then
			(
				SetupScene()
				bt_enable.text = "Disable"
				chk_selfshadows.enabled = shadows
				--playanimation()
			)else(
				bt_enable.text = "Enable"
				--stopanimation()
			)
		)
		
		on RO_TINY_RENDER open do
		(
			unregisterredrawviewscallback RedrawCallback
			gc()
		)
		
		on RO_TINY_RENDER close do
		(
			unregisterredrawviewscallback RedrawCallback
			stopanimation()
		)
		
		on sp_l1 changed arg do
		(
			light_1.multiplier = arg
			RedrawViewports()
		)
		
		on sp_l2 changed arg do
		(
			light_2.multiplier = arg
			RedrawViewports()
		)
		
		on chk_preview changed arg do
		(
			preview = node.showVertexColors = arg
			
			if preview then node.mat.selfIllumAmount = 100 else node.mat.selfIllumAmount = 0
			
			RedrawViewports()
		)
		
		on chk_shadows changed arg do
		(
			shadows = chk_selfshadows.enabled = node.receiveshadows = arg
			RedrawViewports()
		)
		
		on chk_selfshadows changed arg do
		(
			selfShadows = node.castShadows = arg
			RedrawViewports()
		)
		
	)
	
	createdialog RO_TINY_RENDER

)

#45

Thanks for that, i am still working on this.
At the moment I am trying to simulate the default lighting.
I created a testcene where i created the 2 default lights ( defaultKeyLight and defaultFillLight ) and created a camera from view before moving anything.
default_lighting_with_visibile.max (196 KB)

Now I found out how to get the viewport camera position, its direction and its rotation like this:

coordSysTM = inverse (getViewTM())
viewDir = -coordSysTM.row3 -- Get camera direction
viewPos = coordSysTM.row4 -- Get camera position
viewTarget = viewPos+(viewDir*gw.GetFocalDist())
viewRotation = (getViewTM()).rotation -- rotation around viewDir vector -> contains viewRotation.angle in degrees and viewRotation.axis
viewRotationVector = normalize [viewRotation.x, viewRotation.y, viewRotation.z]

Now If you look at the default_light_scene i just uploaded you can see all Rays as splines for a better understanding. My question is, how can i get the red vector only from the information about the viewport?
The plan is to measure out the angle(s) of the default situation and then somehow calculate the defaultLights position based on the viewport cameras position, its direction and its rotation.

My Math is not good enough at the moment. Can anyone help me out with this?
Thanks in advance!

EDIT:
By trial and erroring for hours i stumpled upon something which seems to work. But I dont know what s in viewTM().row2 …

      clearListener()
  fn drawLine pointA pointB aColor: = ( ss = SplineShape pos:pointA; if (aColor != unsupplied) then (ss.wirecolor = aColor); addNewSpline ss; addKnot ss 1 #corner #line PointA; addKnot ss 1 #corner #line PointB; updateShape ss; ss )
--coordSysTM = inverse (viewport.getTM())
viewTM = getViewTM()
coordSysTM = inverse (viewTM)
viewRow2 = -coordSysTM.row2 -- ? no clue what it is
viewDir = -coordSysTM.row3 -- Get camera direction
viewPos = coordSysTM.row4 -- Get camera position
viewTarget = viewPos+(viewDir*gw.GetFocalDist())
print "tee"
--default on startup 2014.94
/*keyLightDist = distance viewTarget $DefaultKeyLight.position
fillLightDist = distance viewTarget $DefaultFillLight.position
*/
defaultLightDist = 2014.94
	v1 = normalize viewDir
v2 = normalize viewRow2
nv1 = normalize (cross v1 v2)
nv2 = normalize (cross nv1 v1)
tempMatrix = matrix3 v1 nv2 nv1 viewPos
/*
drawLine viewPos  (viewPos + v1 * 150) aColor:red
drawLine viewPos  (viewPos + nv2 * 150) aColor:green
drawLine viewPos  (viewPos + nv1 * 150) aColor:blue
*/

--World Point * inverse yourObject.transform -> back to local
--Local Point * yourObject.transform -> world coordinates
--lightPosLocal = $DefaultKeyLight.position * inverse tempMatrix -- default [-1663.51,-627.287,-70.7103]
--print "lightPosLocal = " + (lightPosLocal as string)
defaultKeyLightPosLocal = [-1663.51,-627.287,-70.7103]

defaultKeyLightPos = defaultKeyLightPosLocal * tempMatrix -- word space value
print "lightPosWorld = " + (defaultKeyLightPos as string)
keyLightVector = normalize (defaultKeyLightPos - viewTarget)
drawLine viewTarget (keyLightVector * -defaultLightDist + viewTarget) 

I try now to implement it to calculate the position of the default lights depending on the viewport camera pos. BUT, i dont know if the distance of the default light changes when i zoom out. Does anyone know?