The difference between a macro and a function comes down to scope and the ability to redefine a macro if needed.
A function is fairly straightforward, the values you can access inside a function are limited to those passed into the function or those defined as uniforms (aka the variables defined outside of any function, like material properties or shader globals).
struct v2f {
float4 pos : SV_Position;
float myValue : TEXCOORD0;
};
float _MyMaterialProperty;
void MyFunction(inout float value)
{
// this works
value *= _MyMaterialProperty * 2.0;
// error, no foo defined
// value *= foo;
// error, no i defined
// value *= i.myValue;
}
half4 frag (v2f i) : SV_Target
{
float foo = 5.0;
float bar = 2.0;
MyFunction(bar);
return float4(bar,bar,bar,1.0);
}
A macro on the other hand is code that’s injected directly into the spot it’s called, there for it has access to values that exist at that scope.
struct v2f {
float4 pos : SV_Position;
float myValue : TEXCOORD0;
};
float _MyMaterialProperty;
// totally fine since i exists in the frag function before this macro is called
#define MY_MACRO(value) value *= i.myValue
}
half4 frag (v2f i) : SV_Target
{
float bar = 2.0;
MY_MACRO(bar);
return half4(bar,bar,bar,1.0);
}
Basically, a macro is nearly the same as copy and pasting that code into where it’s called, doing a string replacement for any input parameters.
// this
#define MY_MACRO(value) value *= 2
float var = 1.0;
MY_MACRO(var);
// turns into this
float var = 1.0;
var *= 2;
You might notice that some built in Unity macros have a ; after them and some don’t. Generally the old ones have the semi-colon in the macro definition itself, which means you shouldn’t add one when calling the macro because it’ll put two semi-colons in the final shader. Where newer macros don’t have a semi-colon in the macro definition which makes the rest of the code a bit cleaner and more consistent. Neither is more “correct” than the other, but not having a semi-colon in the macro definition is probably preferable.
Also you could have a macro defined in an included file and override it in the shader if need be, which is something you can’t do with a function.
// in myStuff.cginc
#define MY_MACRO(a) a = 1.0
// in your shader file
#include "myStuff.cginc"
#undef MY_MACRO
#define MY_MACRO(a) a = 2.0
half4 frag(v2f i) : SV_Target
{
float value = 0.0;
// returns 2.0, not 1.0 for value
MY_MACRO(value);
return half4(value, value, value, 1.0);
}