...
To find the vector from the mesh origin to the face simply get the face center and the position of the mesh. Subtract the mesh origin from the face center. Normalize the vector to find the vector direction from origin to face.
| Code Block |
|---|
--Returns the vector direction from the center of iFace on oNode to the origin of oNodefn |
...
GetOriginToFaceDirection oNode iFace = ( |
...
local vFaceCenter = meshop.getfacecenter oNode iFace |
...
local vDiff = vFaceCenter - oNode.position |
...
normalize vDiff |
...
) |
Determine if two faces point generally the same direction
The function "dot" used with two normal vector inputs will produce a value that can be used to determine if something is facing the same direction as another. If the value is negative, then they point in opposite directions. If the value is positive, they point in generally the same direction. If the value is 1, they have the exact same orientation, and if the value is -1, they have the exact opposite orientation. If the value is exactly 0, then the first normal is perpendicular to the second.
| Code Block |
|---|
--Returns 1 if vNormalA faces the same direction as vNormalB, otherwise returns 0 |
...
fn GetIsFacingSameGeneralDirection vNormalA vNormalB = ( |
...
vNormalA = normalize vNormalA |
...
vNormalB = normalize vNormalB |
...
(dot vNormalA vNormalB) > 0 |
...
) |
Determine if the origin is behind a face
If the vector from the origin to the face is pointing the same general direction as the face normal, then the origin is behind the face, otherwise the face is pointing at the origin, meaning the origin is in front of the face.
| Code Block |
|---|
--Returns 1 if origin of oNode is behind face iFace of oNode |
...
fn GetIsOriginBehindFace oNode iFace = ( |
...
local vFaceCenter = meshop.getfacecenter oNode iFace |
...
local vDiff = vFaceCenter - oNode.position |
...
local vNormalA = normalize vDiff |
...
local vNormalB = getfacenormal oNode iFace |
...
(dot vNormalA vNormalB) > 0 |
...
) |
Simple First Test: Set the pivot to the average of all face centers minus their face normal
Assuming you have a good mesh to work from without multiple elements, concave hulls, overlapping faces, etc., this function alone will fix most simple meshes.
| Code Block |
|---|
--Find the average of the face centers, minus the normal of each face, and set the pivot to that position |
...
fn SetPivotToAverageFaceCenter oNode = ( |
...
local vSum = [0,0,0] |
...
local f |
...
for f = 1 to oNode.numfaces do ( |
...
vSum += (meshop.getfacecenter oNode f) - (getfacenormal oNode f) |
...
) |
...
local vAverage = vSum/oNode.numFaces |
...
local vDiff = vAverage - oNode.position |
...
meshop.movevert oNode #{1..oNode.numverts} (oNode.position - vDiff) |
...
oNode.position = vAverage |
...
) |
Second simple test: Check if any face still points at the origin
...
To do that, use this function to check each face using a function above.
| Code Block |
|---|
--Does a simple check to see if shadow pivot position is behind all faces |
...
--Does NOT check if there are overlapped faces from that position |
...
--Returns 1 if all faces on node oNode are in front of the pivot, or 0 if any one face is not |
...
--another position value can be specified in vPos, but if left empty will use the position of oNode |
...
fn CheckShadowPivot oNode vPos:undefined = ( |
...
if vPos == undefined then vPos = oNode.position |
...
local keepIt = true |
...
local f = 1 |
...
while f <= oNode.numfaces and keepIt do ( |
...
local vFaceCenter = meshop.getfacecenter oNode f |
...
local vPointToFace = normalize (vFaceCenter-vPos) |
...
local vFaceNormal = getfacenormal oNode f |
...
local fFaceDotNormal = dot vFaceNormal vPointToFace |
...
if fFaceDotNormal < 0 then ( |
...
keepIt = false |
...
) |
...
f+=1 |
...
) |
...
return keepIt |
...
) |
Snap verts to whole centimeters
...
Here are two functions to make rounding in GMax better.
| Code Block |
|---|
--Returns the nearest integer value to input value n |
...
fn roundNearestInteger n = ( |
...
if (n>0) then (floor (n + 0.5)) else (ceil (n - 0.5)) |
...
) |
...
--Rounds a vector to integer values for x,y,z coordinates |
...
fn roundNearestIntegerVec3 v = ( |
...
for i = 1 to 3 do ( |
...
if (v[i]>0) then (v[i] = floor (v[i] + 0.5)) else (v[i] = ceil (v[i] - 0.5)) |
...
) |
...
v |
...
) |
And here is a function which uses the previous function to snap your mesh to correct positions.
| Code Block |
|---|
--Snaps the mesh and all of its verts to the nearest integer values |
...
fn SnapMeshAndVertsToNearestInteger oNode = ( |
...
oNode.position = roundNearestIntegerVec3 oNode.position |
...
for v = 1 to oNode.numverts do ( |
...
local vert = getvert oNode v |
...
vert = roundNearestIntegerVec3 vert |
...
setvert oNode v vert |
...
) |
...
) |
Your Next Step
If the second test failed, then you know you have more work to do in getting the pivot behind all faces. In addition to that, you also need to check that a ray drawn from the pivot outward can only ever hit one face. A simple test from pivot to each vert will tell you that. Gmax has some ray tracing functions, but they're inherently broken, failing when a backface is struck first. This issue may or may not be fixed in higher versions of Max. For this issue, there is some code I will share with a finished product at a later date. That code has a fully functional ray tracing function with a full report of the faces struck, including their details like normals and face indices.
...
I know these scripts work because they've worked wonders on the Crumbling Crypt set I started building in 2021. Hopefully I can simplify them to work for you too.
Additional Considerations Not Related to the Mesh
Light Node ShadowRadius Value
While everything so far presented here is mesh relative, there is one other issue that does come up sometimes, and that's lightsource related.
Light sources have two properties, Radius and Shadow Radius. By default, shadow radius equals light radius, therefore a torch with max radius 20m will also only cast a shadow to 20m, and a torch of only 2.5m radius also casts only a 2.5m shadow.
In most cases, people make their torch radius with multiplier x1, meaning the intensity is also set by the radius. A longer radius is effectively a stronger light. So a short radius light really wouldn't cast a long shadow anyway, right?
But let's investigate a deep pit in a DARK region where you're using a torch. If the torch max radius is 20m and your pit is 40m, then the bottom of your pit will be brighter than the shadowed section, which will look very odd.
You can fix that issue by modifying your torch lights. OC torches, rings and light effects are going through the file fx_light_clr.mdl. In that model you will see no shadow radius values for any of the lights. If you want your shadows in your world to reach the furthest extents, then you need a ShadowRadius value equal to the extent you want reached.
Take note that a longer shadow requires more drawing time. So setting your values to crazy high numbers will also make more work for the shaders. Likewise setting shadow radius lower will improve draw speed.
Tile Bounding Box
Another pit-related example can be used to examine the next case scenario. Imagine you have a deep pit in a well lit area, and you want the mesh along the pit to shade the pit all the way down.
To get this to occur, you need to have a correct pit-related bounding box. Light will not be drawn below the shadow-casting tile's lower bounds. So for example adjacent to your pit, you may have a flat floor tile with a lower bounds of around 0 elevation. This tile will not shadow very deeply into the pit next door.
To fix this issue, you can simply put a tiny mesh node way down below the tile origin. Place the mesh at the depth of your deepest pit, minus the height of your area height transition. That should work for almost all cases.