Common functions
From Tech Artists Wiki
These are common functions in a variety of languages. Please add these as you find new ones, and convert the existing ones to different languages, especially HLSL, MEL, maxScript, and Python.
[edit] True Luminosity
Use this to take your RGB colors and get their true luminosity. Somewhat expensive, for a real-time implementation see Cheap Luminosity, given below.
[edit] MAXScript
fn RGBToLuminance color = ( local colorP3 = color as point3 local r = colorP3.x local g = colorP3.y local b = colorP3.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) )
[edit] 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) { maxRGB = r ; }
if (g >= r) { maxRGB = g ; }
if (g >= b) { maxRGB = g ; }
if (b >= r) { maxRGB = b ; }
if (b >= g) { 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;
}
[edit] Python
Works with a list or a tuple, as Python doesn't have a built-in colour or vector type.
def RGBToLuminance(colourSeq):
return (max(colourSeq) + min(colourSeq)) / 2.0
[edit] Cheap Luminosity
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.
[edit] MAXScript
fn RGBToLuminanceCheap color = ( color = (color as Point3)/255 --convert 255-based color to floats local lumCoeff = [0.299,0.587,0.114,0.] local luminance = dot color lumCoeff return (luminance * 255) )
This can be optimized as follows (1.6 times faster - 3579 vs 5766 ms for 1 million calls):
fn RGBToLuminanceCheap color = ( ( dot ((color as Point3)/255) [0.299,0.587,0.114,0.] )*255 )
[edit] HLSL
float RGBToLuminanceCheap (float4 color)
{
float4 lumCoeff = [0.299,0.587,0.114,0.];
float luminance = dot(color, lumcCoeff);
return luminance;
}
[edit] Python
def RGBToLuminanceCheap(colourSeq):
lumCoeff = [0.299,0.587,0.114]
return sum(x*y for x,y in zip(colourSeq, lumCoeff))
[edit] Phong Lighting
Phong lighting is using specular based off the reflection vector; compare to Blinn lighting, below.
[edit] HLSL
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;
}
[edit] Blinn Lighting
Blinn lighting is using specular based off the half angle; compare to Phong, above.
[edit] HLSL
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;
}
[edit] Anisotropic Lighting
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 ()
{
}
[edit] Single BRDF
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.).
[edit] HLSL
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;
}
[edit] Dual BRDF
The notes pertaining to Single BRDF apply here. The difference is we use two BRDF textures with a mask to LERP between them.
[edit] HLSL
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;
}
[edit] Offset Mapping
Traditional Offset Mapping, very cheap, merely offsets UV coordinates.
[edit] HLSL
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;
}
[edit] Parallax Occlusion Mapping
A really nice Occlusion technique, but expensive as well. 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.
[edit] HLSL
[edit] Vertex Shader Function
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;
}
[edit] Pixel Shader Function
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;
}
[edit] Normal Map Transforms
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.
[edit] HLSL
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;
}
[edit] Ambient/Reflection Cube Map
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.
[edit] HLSL
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;
}
[edit] Linear Interpolation
MAXScript doesn't have a built-in LERP function, here is a function for it.
[edit] MAXScript
fn lerp minVal maxVal term = (maxVal - minVal) * term + minVal
[edit] Python
def lerp(minVal, maxVal, term):
return (maxVal - minVal) * term + minVal
[edit] Saturate
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.
[edit] MAXScript
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 )
[edit] Python
Takes an optional parameter to handle both floats and 0-255 values.
def saturate(num, floats=True):
if num < 0:
num = 0
elif num > (1 if floats else 255):
num = (1 if floats else 255)
return num
Then saturate colour lists with...
colour = [255, 400, -108] colour = [saturate(x, floats=False) for x in colour] colourF = [1.5, 0.4, -0.43] colourF = [saturate(x) for x in colourF]
[edit] Closest Point on Surface
This function will return the position of the closest point on a surface to a reference point. Also check out MeshProjIntersect in MXS Help.
[edit] MAXScript
fn closestPointOnSurf surf refPos = ( mpi = MeshProjIntersect() mpi.setNode surf mpi.build() mpi.closestFace refPos doubleSided:true closestPoint = mpi.getHitPos() mpi.Free() return closestPoint )
[edit] Is Power of Two
Useful for validating texture sizes.
[edit] MAXScript
fn isPowerOf2 n = ( -- Can't be a power of two if it's not an integer if classOf n != integer then return false bit.and n (n-1) == 0 )
[edit] Python
def isPowerOf2(n):
try:
return n & n-1 == 0
except TypeError: # Catch non-integers
return False
[edit] Shuffle
Produce a random permutation of an array
[edit] MAXScript
fn shuffle data = ( local newData = #() while data.count > 0 do ( local newIndex = random 1 data.count append newData data[newIndex] deleteItem data newIndex ) newData )
A faster version (roughly 2x):
fn QuickShuffle ar = ( fn RandomSort x y = random -1 1 qsort ar RandomSort ar )
[edit] Python
import random data = [1, 2, 3, 4, 5] random.shuffle(data) #Operates in place
