Common functions

From Tech Artists Wiki

Jump to: navigation, search

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.

Contents

[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
Personal tools