Blocky output from shader using tex3D sample

I’m experimenting with a simple color lookup table using a 3D texture, but I’m getting unexpected output from the shader. I’m applying it via a simple blit with material in OnRenderImage, and its implementation looks like this:

Shader "Custom/Lookup"
{
     Properties
     {
         _MainTex("Base (RGB)", 2D) = "white" {}
         _LUT("Lookup table", 3D) = "white" {}
     }

     SubShader
     {
         Cull Off ZWrite Off ZTest Always
         Pass
         {
             CGPROGRAM

                 #pragma vertex vert_img
                 #pragma fragment frag

                 #include "UnityCG.cginc"

                 uniform sampler2D _MainTex;
                 uniform sampler3D _LUT;

                 float4 frag(v2f_img i) : COLOR
                 {
                     float4 color = tex2D(_MainTex, i.uv);
                     float4 converted = tex3D(_LUT, color.rgb);
                     converted.a = color.a;

                     return converted;
                 }

             ENDCG
         }
     }
}

Without the shader applied, I get this output (with the editor’s render scale set to 3x):

With the shader, I get this. Note that there are blocky, discolored pixels at intersections between colors:

My 3D texture is a neutral LUT with 32 pixels in each dimension. I’ve even tried this with a size 256 LUT just to rule out that problem, and although the colors are more accurate (as I would expect), the blocky pixels still show up. If I use sample a 2D texture instead of a 3D texture (e.g. using color.rg), this blockiness doesn’t happen, so it seems to be somehow linked to using a 3D texture.

My guess is that somehow the colors are getting interpolated, but I have anti-aliasing entirely disabled in this project. This same problem happens in both Unity 2019.2.10f1 and 2019.3.0f3.

Any idea what this could be?

Your LUT 3D has mipmaps enabled. When the LUT is generated, make sure you create the Texture3D with no mipmaps. Altneratively use tex2Dlod() to sample the LUT to ensure it only ever samples the top mip level…

Thanks, that did it!

I thought passing a mipcount of 0 to the Texture3D constructor would disable mipmaps, but apparently it doesn’t — I should have been using the constructor that takes a bool mipChain and passed false.