Why does tex3d return a different value than tex2d?

I’m hoping someone can shed some light on how texture sampling works with Texture3D and tex3d. I am seeing different results when sampling from a 2D texture then when sampling from a similar 3D texture.

As a simple test case, I create a 3D texture in code, and have a shader that maps a slice of that 3D texture to a 2D quad. The texture is 8x8x8, and is gradient (or step) from black to white in the x/u direction. So for a given value of x/u, all values of y/v and z/w are the same.

When sampling in the shader, I map the u and v from the vertex positions as you would with any 2D texture, and set the w to a fixed value.
At the same time as I create the 3D texture, I also create a 2D texture, using the same pixel values as the 3D texture for one slice in the z-direction, when z = 0.

Both textures have point filtering and clamp wrap mode.

Why are the colors brighter on the 3D version?

Here is the code to create the textures:

		for(int k = 0; k < texDim; k++)
		{
			for(int j = 0; j < texDim; j++)
			{
				for(int i = 0; i < texDim; i++)
				{
					byte colorByte = (byte)(255 * i / (float)(texDim - 1));
                    pixels[i + (j * texDim) + (k * texDim * texDim)].r = colorByte;
					pixels[i + (j * texDim) + (k * texDim * texDim)].g = colorByte;
					pixels[i + (j * texDim) + (k * texDim * texDim)].b = colorByte;
					pixels[i + (j * texDim) + (k * texDim * texDim)].a = 1;

					if(k == 0)
					{
						pixels2d[i + (j * texDim)].r = colorByte;
						pixels2d[i + (j * texDim)].g = colorByte;
						pixels2d[i + (j * texDim)].b = colorByte;
						pixels2d[i + (j * texDim)].a = 1;
					}
				}
			}
		}
		
		tex3d.SetPixels32(pixels);
		tex3d.wrapMode = TextureWrapMode.Clamp;
		tex3d.filterMode = FilterMode.Point;
		tex3d.Apply();

		tex2d.SetPixels32(pixels2d);
		tex2d.wrapMode = TextureWrapMode.Clamp;
		tex2d.filterMode = FilterMode.Point;
		tex2d.Apply();

Here is the 3d shader code:

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler3D _Tex3d;
			float _Slice;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				float3 uvw = float3(i.uv.x, i.uv.y, _Slice);
				fixed4 col = tex3D(_Tex3d, uvw);
				return col;
			}

And 2D shader code:

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				return col;
			}

Texture2D has sRGB gamma correction by default:

Texture3D does not have an option to choose which color space you use. I believe it is always linear.

Try this code for Texture2D, which makes it linear color space instead of sRGB. Then they should be the same.

tex3d = new Texture3D(texDim, texDim, texDim, TextureFormat.ARGB32, false);
Color32[] pixels = new Color32[texDim * texDim * texDim];

// The final parameter bool is linear color space (true)
tex2d = new Texture2D(texDim, texDim, TextureFormat.ARGB32, false, true);
Color32[] pixels2d = new Color32[texDim * texDim];

I know you mentioned they have they both have point filtering, but I would double check the filterMode property on the textures, it could be changing itself?