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
)
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…
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?
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
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).
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
)
)
@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
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).
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.
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.