What's new

Emulating Nintendo 64 3-sample Bilinear Filtering using Shaders.

ArthurCarvalho

New member
Hello, I'm sorry if I'm posting this in the wrong section.

As most of you know, the way bilinear filtering was implemented on the Nintendo64 is different from PC or any other hardware. In order to lower the price of the hardware at the time, Nintendo did their own hack, and used 3 samples to filter the textures, instead of 4. Thanks to this, when the texture is filtered it creates a hexagonal pattern (that I like to call the "rupee" pattern, because it looks like a rupee from Zelda), instead of a diamond pattern (like it happens on PC). This technique is obviously inferior to the 4 sampled bilinear, as it is not a true bilinear filter, but it gives the Nintendo64's blurry mess it's unique "blurryness". Since most of the textures on the Nintendo64 were pretty low-res, this effect was pretty obvious.

At first, I tried to create a a Hi-res texture pack with the pre-applied filters. It worked, somewhat, but it was hard to apply it to every texture, also the textures didn't tile properly.
I thought if it would be possible to implement it using a HLSL Shader, and I was able to do one.

Now, the reason I've posted this here, it because I'm wondering if some dev would like to try to implement this in his plugin.
I though about posting this in the Direct64 topic, but since this could be implemented on any plugin that could use the programmable pipeline (both DirectX and OpenGL), I decided to post here.

If the plugin is already using the programmable pipeline, I believe it would be pretty straightforward to implement this.
In order for the filter to work, it needs two constants: the texture dimensions, both width and height, also the texture sampler must be point (nearest neighbor) instead of linear, since the shader itself is going to interpolate between texels.

Here's the HLSL code:
Code:
float4 n64BilinearFilter( in float4 vtx_color : COLOR, in float2 texcoord_0 : TEXCOORD0) : COLOR {
	
	float2 tex_pix_a = float2(1/Texture_X,0);
	float2 tex_pix_b = float2(0,1/Texture_Y);
	float2 tex_pix_c = float2(tex_pix_a.x,tex_pix_b.y);
	float2 half_tex = float2(tex_pix_a.x*0.5,tex_pix_b.y*0.5);
	float2 UVCentered = texcoord_0 - half_tex;
	
	float4 diffuseColor = tex2D(ColorSampler,UVCentered);
	float4 sample_a = tex2D(ColorSampler,UVCentered+tex_pix_a);
	float4 sample_b = tex2D(ColorSampler,UVCentered+tex_pix_b);
	float4 sample_c = tex2D(ColorSampler,UVCentered+tex_pix_c);
	
	float interp_x = modf(UVCentered.x * Texture_X, Texture_X);
	float interp_y = modf(UVCentered.y * Texture_Y, Texture_Y);

	if (UVCentered.x < 0)
	{
		interp_x = 1-interp_x*(-1);
	}
	if (UVCentered.y < 0)
	{
		interp_y = 1-interp_y*(-1);
	}
	
	diffuseColor = (diffuseColor + interp_x * (sample_a - diffuseColor) + interp_y * (sample_b - diffuseColor))*(1-step(1, interp_x + interp_y));
	diffuseColor += (sample_c + (1-interp_x) * (sample_b - sample_c) + (1-interp_y) * (sample_a - sample_c))*step(1, interp_x + interp_y);

    return diffuseColor * vtx_color;
}

The pixel interpolation algorithm I got from .oisyn, from this forum:

devmaster.net/forums/topic/13338-bilin-filtering-with-3-samples/

Here's a screenshot: (you'll have to copy and paste the URL, because I couldn't post this link here)

i.imgur.com/oRETY.png

On the left is the standard bilinear filtering implementation, on the right is how Nintendo implemented it in the Nintendo 64.
This is, of course, not inside an emulator, but inside nVidia's FX Composer, where I was coding it. Still, the effect is just like it would look like on the real hardware.

And I understand most people might think of this as a downgrade, and that there is no reason to implement this.
But I think that this is something unique to the Nintendo64, I mean, really, you won't see this kind of filtering anywhere else.
It really gives that "N64" feeling, at least for me it does. It is less smooth, a little more rough than the proper bilinear filter, and I think it gives a little more texture to the game.

So, what do you guys think?
 

angrylion

New member
The exact algorithm the RDP uses for this triangular filtering has been reverse-engineered in MAME/MESS. See method N64TexturePipeT::Cycle() here: http://mamedev.org/source/src/mame/video/rdptpipe.c.html
The RDP decides which three texels out of four to sample depending on the fractional part of a texture coordinate. If I understand your code correctly (having myself zero knowledge of HLSL), you haven't implemented this. Also, the precision of these fractional parts used as interpolation factors is only 5 bits, and everything is done in fixed-point arithmetics.
Anyway, just my two cents. And yeah, the N64 is famous for its jagged texture patterns caused by this filter.
 
Last edited:
OP
A

ArthurCarvalho

New member
I didn't know that MAME had this, thanks.

Originally, based on the algorithm in this topic (that's not the real one though, just something that looks close), it uses an if to detect what pixels to sample,
since (based on my outdated knowledge) that Ifs are no good in shaders, I've done it in a more hacky way:
Code:
diffuseColor = (diffuseColor + interp_x * (sample_a - diffuseColor) + interp_y * (sample_b - diffuseColor))*(1-step(1, interp_x + interp_y));
	diffuseColor += (sample_c + (1-interp_x) * (sample_b - sample_c) + (1-interp_y) * (sample_a - sample_c))*step(1, interp_x + interp_y);
I use the step() function (Intrinsic HLSL function) to black out the bottom right after interpolating, then, I black out the top-left when interpolating the bottom-right pixel, then, I sum up both of them together.
This gives the same effect. I tested both using if and this method and the results were almost the same, if there is any difference, it was pretty small.
 
Last edited:

aliaspider

New member
hi ArthurCarvalho.

your idea has just been implemented into mupen64plus-libretro, and the result looks really great !! it is a definitive improvement to standard bilinear filtering for N64 games in my opinion.

it will become available with the next release of retroarch, but you can already get the source from git and compile it yourself if you want :

https://github.com/libretro/mupen64plus-libretro

you can toggle between bilinear and 3-sample filtering in "core options" in retroarch's main menu (F1).
 
Last edited:
F

Fanatic 64

Guest
This was posted (and revived from) over a year ago.
 
F

Fanatic 64

Guest
...Well yeah. I was half asleep when I wrote that. Maybe this could be implemented in some plugin? Would anybody want to propose it to Gonetz?
 

squarepusher2

New member
Or you could you know... just use mupen64plus libretro right now.

Once this version of Glide64 starts becoming more finished I might decide to make it available as a standalone Zilmar-spec plugin too. Right now it's exclusive to the libretro port and baked in with it.
 
Last edited:
F

Fanatic 64

Guest
Whatever, I like standard bilinear filtering anyway :p

One question, is this texture filtering algorithm integrated in Glide64 itself (in Glide API) or in the wrapper?
 

squarepusher2

New member
Whatever, I like standard bilinear filtering anyway :p

One question, is this texture filtering algorithm integrated in Glide64 itself (in Glide API) or in the wrapper?

Glide64 is a rasterizer that is mostly CPU-bound. It is not really all that much different from the implementation of the RDP found in CEN64/MESS really - just more inaccurate (but it can be fixed I believe).

All the Glide part is used for is texture generation/sampling, vertice processing and final output image generation - and some other stuff that I'm not mentioning right now.


Anyway, I have nearly rewritten Glide64 entirely so that it is more maintainable - and I don't care about supporting 3Dfx Voodoo cards that nobody has or uses anymore - so I have taken liberties with the 'wrapper' part. Don't expect the 3-sample code changes to be easily portable to any existing plugin out so far. It's GLSL too so it would need to be a GL plugin - D3D need not apply.
 

Gonetz

Plugin Developer (GlideN64)
Am I right that author of the original algorithm is ArthurCarvalho and author of GLSL implementation in mupen64plus-libretro is twinaphex ?

And the question regarding the GLSL implementation:
for each read texel the following manipulation is applied:

c = vec4(c.rgb*c.a, c.a);

Why? That makes the algorithm not applicable to textures with zero alpha.
 
Last edited:

Top