_______________________


Rob Galanakis
Technical Artist
516.680.1603
robg@robg3d.com

Common Functions in maxScript and HLSL

Contents:
  1. True Luminosity (HLSL, MXS)
  2. Cheap Luminosity (HLSL, MXS)
  3. Changing Distance Between Two Objects (MXS)
  4. Phong Lighting (HLSL)
  5. Blinn Lighting (HLSL)
  6. Anisotropic Lighting (HLSL)
  7. Single BRDF (HLSL)
  8. Dual BRDF (HLSL)
  9. Offset Mapping (HLSL)
  10. Parallax Occlusion Mapping (HLSL)
  11. Normal Map Transforms (HLSL)
  12. Linear Interpolation (MXS)
  13. Saturate (MXS)
  14. Closest Point on Surface (MXS)

True Luminosity (MXS and HLSL): Use this to take your RGB colors and get their true luminosity. Somewhat expensive, for a real-time implementation see Cheap Luminosity, given below.

fn RGBToLuminance color = (
   local r = color.x
   local g = color.y
   local b = color.z
   local maxRGB = 0
   local minRGB = 0

   if r >= g then maxRGB = r
   if r >= b then maxRGB = r
   if g >= r then maxRGB = g
   if g >= b then maxRGB = g
   if b >= r then maxRGB = b
   if b >= g then maxRGB = b

   if r <= g then minRGB = r
   if r <= b then minRGB = r
   if g <= r then minRGB = g
   if g <= b then minRGB = g
   if b <= r then minRGB = b
   if b <= g then minRGB = b

   lumin = ((maxRGB + minRGB)/2)
)

//HLSL
float RGBToLuminance(float4 color)
{
   float r = color.x;
   float g = color.y;
   float b = color.z;
   float maxRGB;
   float minRGB;
   if (r >= g) { maxRGB = r ; }
   if (r >= b) { then maxRGB = r ; }
   if (g >= r) { then maxRGB = g ; }
   if (g >= b) { then maxRGB = g ; }
   if (b >= r) { then maxRGB = b ; }
   if (b >= g) { then maxRGB = b ; }

   if (r <= g) { minRGB = r ; }
   if (r <= b) { minRGB = r ; }
   if (g <= r) { minRGB = g ; }
   if (g <= b) { minRGB = g ; }
   if (b <= r) { minRGB = b ; }
   if (b <= g) { minRGB = b ; }

   float lumin = ((maxRGB + minRGB)/2);
   return lumin;
}

Cheap Luminosity (MXS and HLSL): If you need to get luminosity real time, this is much cheaper. It uses luminosity weights you can change if you want a different effect.

fn RGBToLuminanceCheap color = (
   color = color/255 --convert 255-based color to floats
   local lumCoeff = [0.299,0.587,0.114,0.]
   local luminance = dot(color, lumcCoeff)
   return (luminance * 255)
)

//HLSL
float RGBToLuminanceCheap (float4 color)
{
   float4 lumCoeff = [0.299,0.587,0.114,0.];
   float luminance = dot(color, lumcCoeff);
   return luminance;
}

Changing the Distance Between Two Objects (MXS): My Answer at CGTalk


Phong Lighting (HLSL): Phong lighting is using specular based off the reflection vector; compare to Blinn lighting, below.

float SpecularPhong (float3 vecLight, float3 vecView, float3 vecNormal, float fGlossiness)
{
   float3 vecReflection = normalize(2 * dot(vecView,vecNormal) * vecNormal - vecView);
   float fRdotL = saturate(dot(vecReflection, vecLight));
   float fSpecular = saturate(pow(fRdotL, gGlossiness));
   return fSpecular;
}

Blinn Lighting (HLSL): Blinn lighting is using specular based off the half angle; compare to Phong, above.

float SpecularBlinn (float3 vecLight, float3 vecView, float3 vecNormal, float fGlossiness)
{
   float3 vecHalf = vecLight + vecView;
   float fNdotH = saturate(dot(vecNormal, vecHalf));
   float fSpecular = pow(fNdotH, fGlossiness);
   return fSpecular;
}

Anisotropic Lighting (HLSL): A note about this anisotropic solution: it uses the mesh's binormal and tangents, which are derived from its UV's. This means its UV's must be layed out either up and down or left and right to get 'proper' anisotropy, distortions in the UV will distort your highlits. Our highlights will either go across the U or across the V, depending on if we use the Tangent or Binormal in our specular calculation.

float4 PlaceholderFunction ()
{

}

Single BRDF (HLSL): We use the NdotL and NdotE to look up on a texture. We allow the NdotL to go from -1 to 1 instead of 0 to 1, so we can push past the physical diffuse (such as if we wanted a BRDF to light fur or a fuzzy surface). The NdotE goes vertical and is saturated to 0 to 1 (-1 to 0 is never visible), so I recommend using a 2x1 aspect ratio BRDF texture. Also, make sure to set your AddressMode on your texture sampler to Clamp or Mirror instead of Wrap, because you may sample outside of the 0 to 1 UV range and thus and this could mess up your lighting (sampling black pixels at the very brightest diffuse point, etc.).

float4 SingleBrdfDiffuse (float3 vecLight, float3 vecView, float3 vecNormal)
{
   float fNdotL = dot(vecNormal, vecLight);
   float fNdotE = saturate(dot(vecNormal, vecView));
   float4 texBrdf = tex2D(samplerBrdf0, float2((fNdotL * .5 + .5), fNdotE));

   return texBrdf;
}

Dual BRDF (HLSL): The notes pertaining to Single BRDF apply here. The difference is we use two BRDF textures with a mask to LERP between them.

float4 DualBrdfDiffuse (float3 vecLight, float3 vecView, float3 vecNormal, float fBrdfMask)
{
   float fNdotL = dot(vecNormal, vecLight);
   float fNdotE = saturate(dot(vecNormal, vecView));
   float4 texBrdf0 = tex2D(samplerBrdf0, float2((fNdotL * .5 + .5), fNdotE));
   float4 texBrdf1 = tex2D(samplerBrdf1, float2((fNdotL * .5 + .5), fNdotE));
   float4 cBrdf = lerp(texBrdf0, texBrdf1, fBrdfMask);

   return cBrdf;
}

Offset Mapping (HLSL): Traditional Offset Mapping, very cheap, merely offsets UV coordinates. See my Parallax article for description.

float4 OffsetMapping (float3 vecLight, float3 vecView, float3 vecNormal)
{
   float fNdotL = dot(vecNormal, vecLight);
   float fNdotE = saturate(dot(vecNormal, vecView));
   float4 texBrdf = tex2D(samplerBrdf0, float2((fNdotL * .5 + .5), fNdotE));

   return texBrdf;
}

Parallax Occlusion Mapping (HLSL): A really nice Occlusion technique, but expensive as well. See my Parallax article for description. Note that there are two steps here. The first needs to go somewhere in the vertex shader (at the end, preferably) and requires some extra work in the globals, structs, etc.; the latter is a function for the pixel shader. See my POM shader on my Shaders page for working examples.

if (useParallaxOcclusion == true)
{
   float3x3 mWorldToTangent = float3x3( Out.vTangentWS, Out.vBinormalWS, Out.vNormalWS );
   Out.vViewTS  = mul( mWorldToTangent, Out.vViewWS  );
        
   // Compute initial parallax displacement direction:
   float2 vParallaxDirection = normalize(  Out.vViewTS.xy );
           
   // The length of this vector determines the furthest amount of displacement:
   float fLength         = length( Out.vViewTS );
   float fParallaxLength = sqrt( fLength * fLength - Out.vViewTS.z * Out.vViewTS.z ) / Out.vViewTS.z; 
           
   // Compute the actual reverse parallax displacement vector:
   Out.vParallaxOffsetTS = vParallaxDirection * fParallaxLength;
           
   // Need to scale the amount of displacement to account for different height ranges
   // in height maps. This is controlled by an artist-editable parameter:
   Out.vParallaxOffsetTS *= g_fOffsetBias;
}

float2 ParallaxOcclusionMapping (float g_nMaxSamples, float g_nMinSamples, float3 vViewWS, 
   float3 vNormalWS, float2 vParallaxOffsetTS, float2 texCoord)
{
  float2 dxSize, dySize;
   float2 dx, dy;
   float2 fTexCoordsPerSize = texCoord;
float4( dxSize, dx ) = ddx( float4( fTexCoordsPerSize, texCoord ) ); float4( dySize, dy ) = ddy( float4( fTexCoordsPerSize, texCoord ) ); int nNumSteps = (int) lerp( g_nMaxSamples, g_nMinSamples, dot( vViewWS, vNormalWS ) );
  float fCurrHeight = 0.0;
  float fStepSize   = 1.0 / (float) nNumSteps;
  float fPrevHeight = 1.0;
  float fNextHeight = 0.0;
  int    nStepIndex = 0;
  bool   bCondition = true;
  float2 vTexOffsetPerStep = fStepSize * vParallaxOffsetTS;
  float2 vTexCurrentOffset = texCoord;
  float  fCurrentBound     = 1.0;
  float  fParallaxAmount   = 0.0;
  float2 pt1 = 0;
  float2 pt2 = 0;
   
  float2 texOffset2 = 0;
  while ( nStepIndex < nNumSteps ) 
  {
     vTexCurrentOffset -= vTexOffsetPerStep;
     // Sample height map which in this case is stored in the alpha channel of the normal map:
fCurrHeight = tex2Dgrad( samplerNormal, vTexCurrentOffset, dx, dy ).a;
     fCurrentBound -= fStepSize;
     if ( fCurrHeight > fCurrentBound ) 
     {   
        pt1 = float2( fCurrentBound, fCurrHeight );
        pt2 = float2( fCurrentBound + fStepSize, fPrevHeight );
        texOffset2 = vTexCurrentOffset - vTexOffsetPerStep;
        nStepIndex = nNumSteps + 1;
        fPrevHeight = fCurrHeight;
     }
     else
     {
        nStepIndex++;
        fPrevHeight = fCurrHeight;
     }
  }   
  float fDelta2 = pt2.x - pt2.y;
  float fDelta1 = pt1.x - pt1.y;
  
  float fDenominator = fDelta2 - fDelta1;
  
  // SM 3.0 requires a check for divide by zero, since that operation will generate
  // an 'Inf' number instead of 0, as previous models (conveniently) did:
  if ( fDenominator == 0.0f )
  {
     fParallaxAmount = 0.0f;
  }
  else
  {
     fParallaxAmount = (pt1.x * fDelta2 - pt2.x * fDelta1 ) / fDenominator;
  }
  
  float2 vParallaxOffset = vParallaxOffsetTS * (1 - fParallaxAmount );
  // The computed texture offset for the displaced point on the pseudo-extruded surface:
  float2 texSampleBase = texCoord - vParallaxOffset;
  float2 texSample = texSampleBase;
    return texSample;
}

Normal Map Transforms (HLSL): I do most of my lighting in World Space, as it gives better accuracy than Tangent Space and the transform cost is negligible (especially considering that many techniques now require world space transforms for things such as cubemap lookups). Here are transforms for a tangent space normal map into DirectX world space (Y-up) and 3dsmax (Z-up). Both return unnormalized so you will need to normalize the result.

float3 NormalMapXFormDX (float3 vecNormalWS, float3 vecTangentWS, float3 vecBinormalWS,
   float4 texNormal)
{
   texNormal = texNormal * 2 - 1;
   texNormal = float4((vecNormalWS * texNormal.z) + (texNormal.x * vecTangentWS + texNormal.y
       * -vecBinormalWS), 1);

   return texNormal.xyz;
}

float3 NormalMapXForm3DS  (float3 vecNormalWS, float3 vecTangentWS, float3 vecBinormalWS,
   float4 texNormal)
{
   texNormal = texNormal * 2 - 1;
   texNormal = (vecNormalWS * texNormal.z) + (texNormal.x * vecBinormalWS + texNormal.y
      * -vecTangentWS);

   return texNormal.xyz;
}
Ambient/Reflection Cube Map
(HLSL):Based on ideas from ShaderFX, this will lerp between a reflection cube map and ambient cube map (diffusely convolved or a lower mip level) based on a reflection mask.
   float4 CubeReflectionAmbient (float3 vecNormal, float4 texDiffuse, float fReflectionMask, float fAmbientIntensity)
{
   float4 texReflection = texCUBE(samplerEnvironment, vecNormal);
   float4 texAmbient = texCUBE(samplerAmbient, vecNormal);
   float4 cAmbient = lerp(texAmbient, texReflection, fReflectionMask);
   cAmbient *= texDiffuse * fAmbientIntensity;

   return cAmbient;
}

Linear Interpolation (MXS): MaxScript doesn't have a built-in LERP function, here is a function for it.
fn lerp minVal maxVal term = (maxVal - minVal) * term + minVal

Saturate (MXS): MaxScript doesn't have a built in Saturate, here is a function that will saturate any type of number. Saturate is to clamp a value to 0 and 1, or in the case of a color, 0 and 255.
fn saturate val = (
 
 fn saturateF val = (
  local newVal
  if val < 0 then newVal = 0
  else if val > 1 then newVal = 1
  else newVal = val
  return newVal
 )
 
 local isValColor = 0
 
 if classOf val == Color then (
  --the multiplication or division by 255 for casting a color to/from a point3 or point4 is done implicitly
  isValColor = 1
  try (
   a = val.a --if it crashes, we have a 3-component color
   val = val as point4
  )
  catch (val = val as point3)
 )
 
 local newVal = val
  
 try (newVal.x = saturateF val.x)
 catch (newVal = saturateF val)
 
 try (newVal.y = saturateF val.y)
 catch (return newVal)
 
 try (newVal.z = saturateF val.z)
 catch (return newVal)
 
 try (newVal.w = saturateF val.w)
 catch (return newVal)
 if isValColor == 1 then (
  newVal = newVal as color
 )
 
 return newVal
)
Closest Point on Surfacee MXS): This function will return the position of the closest point on a surface to a reference point. Thanks to original creators, from this CGTalk thread. For more info, visit the thread and check out MeshProjIntersect in MXS Help.
fn closestPointOnSurf surf refPos = (
 mpi = MeshProjIntersect()
 mpi.setNode surf
 mpi.build()
 
 mpi.closestFace refPos doubleSided:true
 
 closestPoint = mpi.getHitPos()
 
 mpi.Free()
 return closestPoint
)