[Optimization] Better implementation of Helpers.cginc : ApplyHue
OwenTheProgram
The function
ApplyHue
in Packages/com.vrchat.base/Runtime/VRCSDK/Sample Assets/Shaders/Mobile/ToonStandard/CG/Helpers.cginc
I felt could be rewritten, here are my findings if you so choose to use them.The original ApplyHue function compiles to this shader code:
0: sample r0.xyzw, v1.xyxx, t0.xyzw, s0
1: eq r0.w, cb0[2].x, l(0.000000)
2: if_z r0.w
3: sincos r1.x, r2.x, cb0[2].x
4: mul r1.yzw, r0.zzxy, l(0.000000, 0.577350, 0.577350, 0.577350)
5: mad r1.yzw, r0.zzxy, l(0.000000, 0.577350, 0.577350, 0.577350), -r1.wwyz
6: mul r1.xyz, r1.xxxx, r1.yzwy
7: mad r1.xyz, r0.xyzx, r2.xxxx, r1.xyzx
8: dp3 r0.w, l(0.577350, 0.577350, 0.577350, 0.000000), r0.xyzx
9: mul r0.w, r0.w, l(0.577350)
10: add r1.w, -r2.x, l(1.000000)
11: mad r1.xyz, r0.wwww, r1.wwww, r1.xyzx
12: add r1.xyz, -r0.xyzx, r1.xyzx
13: mad r0.xyz, v1.zzzz, r1.xyzx, r0.xyzx
14: endif
15: mov o0.xyz, r0.xyzx
16: mov o0.w, l(1.000000)
17: ret
temp registers: r0.xyzw, r1.xyzw, r2.x
- mad x 4
- mul x 3
- add x 2
- sincos x 1
- dp3 x 1
None of these instructions are inherently expensive, in fact they're some of the cheapest outside of sincos, but I love bringing more ways to do things to the table.
My first method is shorter in assembly and only uses the cosine part of sincos.
// Rotates white-point to Z axis, rotates around Z, undo white-point rotation.
half3 ApplyHue(half3 col, half hueAngle, half mask)
{
UNITY_BRANCH
if (hueAngle == 0)
{
return col;
}
else
{
const half PI = 3.14159265359;
const half3 OFFSETS = PI * half3(0.0/3.0, 2.0/3.0, 4.0/3.0);
half3 ct = (1.0/3.0) + (2.0/3.0) * cos(hueAngle + OFFSETS);
half3 shifted = (col.rgb * ct.x) + (col.gbr * ct.y) + (col.brg * ct.z);
return lerp(col, shifted, mask);
}
}
Which compiles to the following with the same usage
0: sample r0.xyzw, v1.xyxx, t0.xyzw, s0
1: eq r0.w, cb0[2].x, l(0.000000)
2: if_z r0.w
3: add r1.xyz, cb0[2].xxxx, l(0.000000, 2.094395, 4.188790, 0.000000)
4: sincos null, r1.xyz, r1.xyzx
5: mad r1.xyz, r1.xyzx, l(0.666667, 0.666667, 0.666667, 0.000000), l(0.333333, 0.333333, 0.333333, 0.000000)
6: mul r2.xyz, r0.yzxy, r1.yyyy
7: mad r1.xyw, r0.xyxz, r1.xxxx, r2.xyxz
8: mad r1.xyz, r0.zxyz, r1.zzzz, r1.xywx
9: add r1.xyz, -r0.xyzx, r1.xyzx
10: mad r0.xyz, v1.zzzz, r1.xyzx, r0.xyzx
11: endif
12: mov o0.xyz, r0.xyzx
13: mov o0.w, l(1.000000)
14: ret
temp registers: r0.xyzw, r1.xyzw, r2.xyz
- mad x 4
- add x 2
- mul x 1
- sincos x 1
This version uses slightly less instructions total, but parallelizes into three components used for r2, which might bump the temporary register count in some cases by one. This annoyed me, so I decided to make a slightly different version that's more in the middle while still using the same component count.
// Rotates white-point to Z axis, rotates around Z, undo white-point rotation.
half3 ApplyHue(half3 col, half hueAngle, half mask)
{
UNITY_BRANCH
if (hueAngle == 0)
{
return col;
}
else
{
const half k = 0.57735026919; //1/sqrt(3)
half ct = cos(hueAngle) * (1.0/3.0);
half st = sin(hueAngle) * k;
half a = -2.0/3.0 + ct + ct;
half b = +1.0/3.0 - ct - st;
half c = +1.0/3.0 - ct + st;
half3 shifted = col.rgb*a + col.gbr*b + col.brg*c;
return col.rgb + mask * shifted;
}
}
Which still manages to strip the original down by a bit.
0: sample r0.xyzw, v1.xyxx, t0.xyzw, s0
1: eq r0.w, cb0[2].x, l(0.000000)
2: if_z r0.w
3: sincos r1.x, r2.x, cb0[2].x
4: mad r0.w, r2.x, l(0.666667), l(-0.666667)
5: mad r1.y, -r2.x, l(0.333333), l(0.333333)
6: mad r1.z, -r1.x, l(0.577350), r1.y
7: mad r1.x, r1.x, l(0.577350), r1.y
8: mul r1.yzw, r0.yyzx, r1.zzzz
9: mad r1.yzw, r0.xxyz, r0.wwww, r1.yyzw
10: mad r1.xyz, r0.zxyz, r1.xxxx, r1.yzwy
11: mad r0.xyz, v1.zzzz, r1.xyzx, r0.xyzx
12: endif
13: mov o0.xyz, r0.xyzx
14: mov o0.w, l(1.000000)
15: ret
temp registers: r0.xyzw, r1.xyzw, r2.x
- mad x 7
- sincos x 1
- mul x 1
Optionally, if you wish to make the range of both of these, you can still simply multiply
hueAngle
by UNITY_TWO_PI
, to resolve feedback like this oneI have also verified the shader compiler does unroll everything to become a single mov instruction if
mask
= 0.I have made supplemental demos in Desmos to showcase the methods:
I hereby grant legal rights to all shader code presented in this feature request. I present all of the above, granting full rights to use, reuse, modify, redistribute, and will not persue any form of legal action, following the guidelines of the MIT-0 No Attribution licence. blablabla open source, use it as you wish!
ApplyHueMinimal.shader (which I have renamed to .txt for sending) has the MIT-0 licence as a header segment by the way.
Log In
OwenTheProgram
ah I forgot to mention as well, it may be wise to saturate the hue shifted colour, because rotating in both ways can result in non 0 to 1 ranges for red green and blue.