gonetz / GLideN64

A new generation, open-source graphics plugin for N64 emulators.
Other
754 stars 174 forks source link

Replace noise textures with hashing #2833

Closed scurest closed 2 months ago

scurest commented 2 months ago

Shaders currently get noise by sampling a 640x580 noise texture with the fragment location. Variation over time is provided by keeping a pool of 30 noise textures and binding a different one every frame.

This PR replaces this with hashing the fragment location and a frame-varying seed value, obviating the need for noise textures.

// Before
uniform sampler2D uTexNoise;
noise = texture2D(uTexNoise, coord).r;

// After
uniform float uNoiseSeed;
noise = hash(vec3(uNoiseSeed, coord));

Noise seed

uNoiseSeed is updated every frame with the low byte of the frame count, ie. it's a simple counter with period 256. This is unlike the noise textures, which would cycle in a random order with long period (but never repeating on two consecutive frames). I don't think this matters, but I can easily change it to the old behavior if you like.

The seed changes automatically every frame and does not need an explicit update() when noise is used.

Hash function

The hash functions used are Dave Hoskins's Hashing without Sine GLSL functions that take 1-4 integer-spaced float inputs and produce 1-4 float outputs in [0,1).

Noise resolution

One noise value is generated per native-res pixel, ie. as by

ivec2 coord = ivec2(gl_FragCoord.xy/uScreenScale);
noise = texelFetch(uTexNoise, coord, 0).r;

To match this, the hash function is passed the native-res pixel location

vec2 coord = floor(gl_FragCoord.xy/uScreenScale);

Note that, as pointed out in #1474, generating high-res noise would be as easy as switching to vec2 coord = gl_FragCoord.xy. But that is not proposed in this PR.

Correlations between noise functions

Previously snoise(), snoiseRGB().r, and snoiseA() were always the same, because they sampled the noise texture at the same location. Does this property need to be maintained? snoiseRGB().r will now be different than the others, because it uses a different hash function.

Font atlas

All noise texture code has been removed, except the noiseFormat etc. variables. These are also used for the font atlas, so they've been renamed fontFormat, etc. to reflect their current use.

Testing

snoise() function works in the Kirby 64 intro.

I have not tested the snoiseRGB() or snoiseA() functions, in particular the enableHiresNoiseDithering option. I'm happy to do so if you can tell me how/where to test it.

Thanks for considering.

mudl0rd commented 2 months ago

Thanks for having a proper look at what I did back then. :)

gonetz commented 2 months ago

@scurest Very accurate work. The result is really good. I tested several cases where noise is used and didn't find any regressions. The noise looks pretty random, as good as with noise textures. Thanks!

Previously snoise(), snoiseRGB().r, and snoiseA() were always the same, because they sampled the noise texture at the same location. Does this property need to be maintained?

I think, no. We need a random input for each color channel, that's all.

snoiseRGB().r will now be different than the others, because it uses a different hash function.

ok.

I have not tested the snoiseRGB() or snoiseA() functions, in particular the enableHiresNoiseDithering option. I'm happy to do so if you can tell me how/where to test it.

As I remember, snoiseA() is used in SM64 with Vanish Cap (invisibility hat). snoiseRGB() is used for noise color dithering. It sometimes is used in cut-scenes. As I remember, it is used in Zelda MM intro, when Link falls into the pit, and in "remembering Zelda" black-and-white cut-scene. I checked these scenes, no problems found.

@mudl0rd

Thanks for having a proper look at what I did back then. :)

Yes, the same idea, but seems to work properly this time.

scurest commented 1 month ago

@gonetz So this broke shader compilation on GLES (#2837) because

uniform float uNoiseSeed;

needs to have a precision qualifier

uniform mediump float uNoiseSeed;

which is an easy fix.

However now that I think about precision, I'm pretty sure the hash functions require highp for p3; if mediump is a float16, the noise will always be 0. This creates a problem for old GLES implementations that do not have highp. Can you confirm if those are supported? If they are, this will need to reverted, or the hashing constant will need to be tweaked somehow.

It looks to me like GLideN64 does try to support them (by replacing highp with mediump)

https://github.com/gonetz/GLideN64/blob/b021d8ee437266cfdd7251daf8c23203578b02b6/src/Graphics/OpenGLContext/GLSL/glsl_CombinerProgramBuilderCommon.cpp#L32-L34