Search Unity

Shader ideas for replicating N64 bi linear filtering

Discussion in 'Shaders' started by Wolfgabe, Jul 22, 2020.

  1. Wolfgabe

    Wolfgabe

    Joined:
    Sep 4, 2016
    Posts:
    131
    so I have been thinking about replicating N64 graphics in Unity and my main challenge so far is figuring out how to emulate the N64 unique method of bi linear filtering. To those who don't know the N64 used 3 samples for filtering rather than the standard 4. I found this link right here that shows off a method of replicating N64 filtering and I was wondering if it could perhaps be done via custom shader in Unity or might there be another way
    http://www.emutalk.net/threads/54215-Emulating-Nintendo-64-3-sample-Bilinear-Filtering-using-Shaders
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,350
    Yes, absolutely. The code shown in that first post is HLSL shader code, the same shader language Unity's shaders are written in. Most of that code would be direct applicable in a Unity shader directly.

    Here's a version that uses a slightly different approach:
    Code (CSharp):
    1. Shader "Custom/N64Filter"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 200
    11.  
    12.         CGPROGRAM
    13.         #pragma surface surf Lambert
    14.  
    15.         sampler2D _MainTex;
    16.         float4 _MainTex_TexelSize;
    17.  
    18.         struct Input
    19.         {
    20.             float2 uv_MainTex;
    21.         };
    22.  
    23.         // based on https://www.shadertoy.com/view/wdy3RW
    24.         // with proper support for mip maps and textures that aren't using point filtering
    25.         fixed4 N64Filtering(sampler2D tex, float2 uv, float4 texelSize)
    26.         {
    27.             // texel coordinates
    28.             float2 texels = uv * texelSize.zw;
    29.  
    30.             // calculate mip level
    31.             float2 dx = ddx(texels);
    32.             float2 dy = ddy(texels);
    33.             float delta_max_sqr = max(dot(dx, dx), dot(dy, dy));
    34.             float mip = max(0.0, 0.5 * log2(delta_max_sqr));
    35.  
    36.             // scale texel sizes and texel coordinates to handle mip levels properly
    37.             float scale = pow(2,floor(mip));
    38.             texelSize.xy *= scale;
    39.             texelSize.zw /= scale;
    40.             texels = texels / scale - 0.5;
    41.  
    42.             // calculate blend for the three points of the tri-filter
    43.             float2 fracTexels = frac(texels);
    44.             float3 blend = float3(
    45.                 abs(fracTexels.x+fracTexels.y-1),
    46.                 min(abs(fracTexels.xx-float2(0,1)), abs(fracTexels.yy-float2(1,0)))
    47.             );
    48.  
    49.             // calculate equivalents of point filtered uvs for the three points
    50.             float2 uvA = (floor(texels + fracTexels.yx) + 0.5) * texelSize.xy;
    51.             float2 uvB = (floor(texels) + float2(1.5, 0.5)) * texelSize.xy;
    52.             float2 uvC = (floor(texels) + float2(0.5, 1.5)) * texelSize.xy;
    53.  
    54.             // sample points
    55.             fixed4 A = tex2Dlod (tex, float4(uvA, 0, mip));
    56.             fixed4 B = tex2Dlod (tex, float4(uvB, 0, mip));
    57.             fixed4 C = tex2Dlod (tex, float4(uvC, 0, mip));
    58.  
    59.             // blend and return
    60.             return A * blend.x + B * blend.y + C * blend.z;
    61.         }
    62.  
    63.         void surf (Input IN, inout SurfaceOutput o)
    64.         {
    65.             fixed4 c = N64Filtering(_MainTex, IN.uv_MainTex, _MainTex_TexelSize);
    66.             o.Albedo = c.rgb;
    67.         }
    68.         ENDCG
    69.     }
    70.     FallBack "Diffuse"
    71. }
     
    PeachClamNine and Invertex like this.
  3. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,550
    Looks like bgolus beat me to it. But anyways here is a direct adaption of the code in the linked example (for some reason I had to change the multiply to an add in the modf() calls, I'm not sure why their implementation works with multiply, as it reduces the blend to nothing.)

    Code (CSharp):
    1. Shader "Invertex/Custom/N64Bilinear"
    2. {
    3.     //Unity implementation of N64 3-point Bilinear Filtering example from:
    4.     //http://www.emutalk.net/threads/54215-Emulating-Nintendo-64-3-sample-Bilinear-Filtering-using-Shaders
    5.     Properties
    6.     {
    7.         _MainTex ("Texture", 2D) = "white" {}
    8.         _Cutoff("Alpha Cutoff", Range(0,1)) = 0.5
    9.     }
    10.     SubShader
    11.     {
    12.         Tags { "RenderType"="Opaque" "Queue"="AlphaTest" }
    13.  
    14.         CGPROGRAM
    15.  
    16.         #pragma surface surf Lambert alphatest:_Cutoff
    17.  
    18.         sampler2D _MainTex;
    19.         float4 _MainTex_TexelSize; //Unity will fill this in with the texture dimensions
    20.  
    21.         struct Input
    22.         {
    23.             float2 uv_MainTex;
    24.         };
    25.  
    26.         fixed4 N64Sample(sampler2D tex, float2 uv, float4 texelSize)
    27.         {
    28.             float Texture_X = texelSize.x;
    29.             float Texture_Y = texelSize.y;
    30.             float2 tex_pix_a = float2(Texture_X, 0.0);
    31.             float2 tex_pix_b = float2(0.0, Texture_Y);
    32.             float2 tex_pix_c = float2(tex_pix_a.x, tex_pix_b.y);
    33.             float2 half_tex = float2(tex_pix_a.x * 0.5, tex_pix_b.y * 0.5);
    34.             float2 UVCentered = uv - half_tex;
    35.  
    36.             float4 diffuseColor = tex2D(tex, UVCentered);
    37.             float4 sample_a = tex2D(tex, UVCentered + tex_pix_a);
    38.             float4 sample_b = tex2D(tex, UVCentered + tex_pix_b);
    39.             float4 sample_c = tex2D(tex, UVCentered + tex_pix_c);
    40.  
    41.             float interp_x = modf(UVCentered.x + Texture_X, Texture_X);
    42.             float interp_y = modf(UVCentered.y + Texture_Y, Texture_Y);
    43.  
    44.             if (UVCentered.x < 0) { interp_x = 1 - interp_x * -1; }
    45.             if (UVCentered.y < 0) { interp_y = 1 - interp_y * -1; }
    46.  
    47.             diffuseColor = (diffuseColor + interp_x * (sample_a - diffuseColor) + interp_y * (sample_b - diffuseColor)) * (1 - step(1, interp_x + interp_y));
    48.             diffuseColor += (sample_c + (1 - interp_x) * (sample_b - sample_c) + (1 - interp_y) * (sample_a - sample_c)) * step(1, interp_x + interp_y);
    49.        
    50.             return diffuseColor;
    51.         }
    52.  
    53.         void surf(Input IN, inout SurfaceOutput o)
    54.         {
    55.             fixed4 c = N64Sample(_MainTex, IN.uv_MainTex, _MainTex_TexelSize);
    56.             o.Albedo = c.rgb;
    57.             o.Alpha = c.a;
    58.         }
    59.         ENDCG
    60.     }
    61.         FallBack "Diffuse"
    62. }
    Note: Make sure the import settings on your textures have the "Filter Mode" set to "Point" instead of Bilinear.

    edit: After further testing I don't think this version is still working correctly. I'm not sure why their math is resulting in different behavior in Unity... So just use bgolus's
     
    Last edited: Jul 22, 2020
  4. SameDev

    SameDev

    Joined:
    Apr 14, 2013
    Posts:
    18
    Is there a way to apply this as a URP rendering pass?
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,350
    Can't be done as a rendering pass. Would have to be implemented in Shader Graph and the resulting shader used on all objects.
     
  6. SameDev

    SameDev

    Joined:
    Apr 14, 2013
    Posts:
    18
    Could you please assist me in converting my shader to URP? I have added vertex color functionality to create more genuine looking art. Here it is:

    Code (CSharp):
    1. Shader "Cyclopian/N64_Vertex"
    2. {
    3.     Properties
    4.     {
    5.         _Color("Color", Color) = (1,1,1,1)
    6.         _MainTex("Albedo (RGB)", 2D) = "white" {}
    7.     [Range(0,1)]
    8.         _Cutoff("Alpha Cutoff", Range(0,1)) = 0.5
    9.     [Range(0,1)]
    10.         _Glossiness("Smoothness", Range(0,1)) = 0.5
    11.     [Range(0,1)]
    12.         _Metallic("Metallic", Range(0,1)) = 0.0
    13.     }
    14.         SubShader
    15.     {
    16.         Tags { "RenderType" = "Opaque" }
    17.         LOD 200
    18.         CGPROGRAM
    19.         #pragma surface surf Standard alphatest:_Cutoff fullforwardshadows
    20.         sampler2D _MainTex;
    21.         float4 _MainTex_TexelSize;
    22.         struct Input
    23.         {
    24.             float2 uv_MainTex;
    25.             float4 vertexColor : COLOR;
    26.         };
    27.         struct v2f {
    28.             float4 pos : SV_POSITION;
    29.             fixed4 color : COLOR;
    30.         };
    31.         fixed4 N64Filtering(sampler2D tex, float2 uv, float4 scale)
    32.         {
    33.             //texel coords
    34.             float2 texel = uv * scale.zw;
    35.             //get mip map coords and scaling
    36.             float2 mipX = ddx(texel), mipY = ddy(texel);
    37.             float delta_max_sqr = max(dot(mipX, mipX), dot(mipY, mipY));
    38.             float mip = max(0.0, 0.5 * log2(delta_max_sqr));
    39.            
    40.             float size = pow(2, floor(mip));
    41.             scale.xy *= size;
    42.             scale.zw /= size;
    43.             texel = texel / size - 0.5;
    44.             //sample points
    45.             float2 fracTexl = frac(texel);
    46.             float2 uv1 = (floor(texel + fracTexl.yx) + 0.5) * scale.xy;
    47.             fixed4 out1 = tex2Dlod(tex, float4(uv1, 0, mip));
    48.             float2 uv2 = (floor(texel) + float2(1.5, 0.5)) * scale.xy;
    49.             fixed4 out2 = tex2Dlod(tex, float4(uv2, 0, mip));
    50.             float2 uv3 = (floor(texel) + float2(0.5, 1.5)) * scale.xy;
    51.             fixed4 out3 = tex2Dlod(tex, float4(uv3, 0, mip));
    52.             //calculate blend and apply
    53.             float3 blend = float3(abs(fracTexl.x + fracTexl.y - 1), min(abs(fracTexl.xx - float2(0, 1)), abs(fracTexl.yy - float2(1, 0))));
    54.             float4 _outTex = out1 * blend.x + out2 * blend.y + out3 * blend.z;
    55.             // blend and return
    56.             return _outTex;
    57.         }
    58.         half _Glossiness;
    59.         half _Metallic;
    60.         fixed4 _Color;
    61.         void surf(Input IN, inout SurfaceOutputStandard o)
    62.         {
    63.             fixed4 c = N64Filtering(_MainTex, IN.uv_MainTex, _MainTex_TexelSize) * _Color * IN.vertexColor;;
    64.             o.Albedo = c.rgb * IN.vertexColor; // Combine normal color with the vertex color
    65.             // Metallic and smoothness come from slider variables
    66.              o.Metallic = _Metallic;
    67.              o.Smoothness = _Glossiness;
    68.              o.Alpha = c.a;
    69.         }
    70.         ENDCG
    71.     }
    72.         FallBack "Diffuse"
    73. }
    74.  
    Any help would be appreciated, even just some pointers. Ive been working on this for a few days now and am lost in where to begin!
     
    Last edited: Dec 1, 2020
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,350
    You need to use Shader Graph to make a shader that works with the URP.
     
  8. The64thGamer_

    The64thGamer_

    Joined:
    Aug 4, 2019
    Posts:
    14
    Are there any good pointers for integrating this into the Shader Graph for URP? Tried messing with custom functions, but couldn't get any sort of reasonable output.
     
  9. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,550
    Just make a custom function with Sampler2D, float2 and float4 as input parameters, named the same as in that code. And make an output parameter of type float4 _outTex, and then just copy the inner code of that N64Filtering function into the custom function, but remove the
    return _outTex
    line and make the line above it just be
    _outTex = out1 * ...etc
    instead of
    float4 _outTex =
     
  10. The64thGamer_

    The64thGamer_

    Joined:
    Aug 4, 2019
    Posts:
    14
    I did that, but I don't think Shader Graph supports a Sampler2D with a texture? The only input parameter equating to Sampler2D is "Bare Sampler State", which only takes in a Sampler State node and never any texture.

     
  11. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,550
    sampler2D
    is older builtin syntax that automatically defines a
    SamplerState
    bound to the
    Texture2D
    .
    So you just need to make a
    Texture2D
    input along with that
    SamplerState
    input.
    Then replace your
    tex2dLod
    functions with
    texture2DinputName.SampleLevel(texSampler, uv, mip)
     
  12. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    Here's my attempt at converting bgolus's shader above:

    Untitled.png

    Code (csharp):
    1. //UNITY_SHADER_NO_UPGRADE
    2. #ifndef MAJORAS_MASK_IS_BETTER_THAN_OCARAINA_OF_TIME_COME_AT_ME_BRO_INCLUDED
    3. #define MAJORAS_MASK_IS_BETTER_THAN_OCARAINA_OF_TIME_COME_AT_ME_BRO_INCLUDED
    4. #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    5. #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
    6.  
    7. void N64Sample_float(
    8.     in UnityTexture2D Texture,
    9.     in float2 UV,
    10.     out float4 Out)
    11. {
    12.     // texel coordinates
    13.     float4 texelSize = Texture.texelSize;
    14.     float2 texels = UV * texelSize.zw;
    15.  
    16.     // calculate mip level
    17.     float2 dx = ddx(texels);
    18.     float2 dy = ddy(texels);
    19.     float delta_max_sqr = max(dot(dx, dx), dot(dy, dy));
    20.     float mip = max(0.0, 0.5 * log2(delta_max_sqr));
    21.  
    22.     // scale texel sizes and texel coordinates to handle mip levels properly
    23.     float scale = pow(2,floor(mip));
    24.     texelSize.xy *= scale;
    25.     texelSize.zw /= scale;
    26.     texels = texels / scale - 0.5;
    27.  
    28.     // calculate blend for the three points of the tri-filter
    29.     float2 fracTexels = frac(texels);
    30.     float3 blend = float3(
    31.         abs(fracTexels.x+fracTexels.y-1),
    32.         min(abs(fracTexels.xx-float2(0,1)), abs(fracTexels.yy-float2(1,0)))
    33.     );
    34.  
    35.     // calculate equivalents of point filtered uvs for the three points
    36.     float2 uvA = (floor(texels + fracTexels.yx) + 0.5) * texelSize.xy;
    37.     float2 uvB = (floor(texels) + float2(1.5, 0.5)) * texelSize.xy;
    38.     float2 uvC = (floor(texels) + float2(0.5, 1.5)) * texelSize.xy;
    39.  
    40.     // sample points
    41.     float4 A = Texture.SampleLevel(Texture.samplerstate, uvA, mip);
    42.     float4 B = Texture.SampleLevel(Texture.samplerstate, uvB, mip);
    43.     float4 C = Texture.SampleLevel(Texture.samplerstate, uvC, mip);
    44.  
    45.     // blend and return
    46.     Out = A * blend.x + B * blend.y + C * blend.z;
    47. }
    48. #endif
    Sorry I don't know enough about the Shader Graph to fully graphiphy it; I just use custom functions for anything that's going to be more than like 4 nodes big.

    PS Happy Thanksgiving to Americans!
     
    Last edited: Nov 25, 2021
    diegosainz3d likes this.