godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.17k stars 21.2k forks source link

Vulkan: Water shader looks different between 3.x (GLES3) and 4.0 #63260

Open BenjaminNavarro opened 2 years ago

BenjaminNavarro commented 2 years ago

Godot version

4.0 dev (e44a2f1680)

System information

Linux, RTX3080, NVIDIA drivers

Issue description

While porting my game prototype from 3.4 to 4.0 I noticed that the water shader I was using looks different.

The shader originaly comes from this video, but I made some small modifications to it (see attached projects).

Here is what the sample project on 3.4 gives: low_poly_water_3_4

And the same with 4.0: low_poly_water_4_0

I tried to match the environments as much as possible. All the shader parameters are the same in both versions.

To me there are two things wrong here:

  1. The transparency: With 3.4 there is some slight transparency around the boxes (easier to see when opening the project) and the plane below the water is not visible. With 4.0, the water is completely invisible when it is above the plane.
  2. The color: the final water color is not the same between the two versions and I think that with 3.4 the color is closer to what it should be (if you compare it to the material color param).

Here is the shader code (4.0 version but only the color hint is different):

shader_type spatial;

uniform vec4 out_color : source_color = vec4(0.0, 0.2, 1.0, 1.0);
uniform float amount : hint_range(0.2, 1.5) = 0.8;
uniform float beer_factor = 0.2;

float generateOffset(float x, float z, float val1, float val2, float time) {
    float speed = 1.0;

    float radiansX = ((mod(x + z * x * val1, amount) / amount) + (time * speed) * mod(x * 0.8 + z, 1.5)) * 2.0 * 3.14;
    float radiansZ = ((mod(val2 * (z * x + x * z), amount) / amount) + (time * speed) * 2.0 * mod(x, 2.0)) * 2.0 * 3.14;

    return amount * 0.5 * (sin(radiansZ) + cos(radiansX));
}

vec3 applyDistortion(vec3 vertex, float time) {
    float xd = generateOffset(vertex.x, vertex.z, 0.2, 0.1, time);
    float yd = generateOffset(vertex.x, vertex.z, 0.1, 0.3, time);
    float zd = generateOffset(vertex.x, vertex.z, 0.15, 0.2, time);
    return vertex + vec3(xd, yd, zd);
}

void vertex() {
    VERTEX = applyDistortion(VERTEX, TIME * 0.1);
}

void fragment() {
    NORMAL = normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));
    METALLIC = 0.6;
    SPECULAR = 0.5;
    ROUGHNESS = 0.2;
    ALBEDO = out_color.rgb;

    float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r;
    depth = depth * 2.0 - 1.0;
    depth = PROJECTION_MATRIX[3][2] / (depth + PROJECTION_MATRIX[2][2]);
    depth = depth + VERTEX.z;

    depth = exp(-depth * beer_factor);
    ALPHA = clamp(1.0 - depth, 0.0, 1.0);
}

Steps to reproduce

See attached projects for reproduction

Minimal reproduction project

LowPolyWaterShader_3_4.zip

LowPolyWaterShader_4_0.zip

clayjohn commented 2 years ago

From a quick look, you will need to change your depth calculation. In Vulkan you do not scale to the -1 to 1 range, so this:

float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r;
depth = depth * 2.0 - 1.0;
depth = PROJECTION_MATRIX[3][2] / (depth + PROJECTION_MATRIX[2][2]);
depth = depth + VERTEX.z;

becomes this:

float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r;
depth = PROJECTION_MATRIX[3][2] / (depth + PROJECTION_MATRIX[2][2]);
depth = depth + VERTEX.z;

For me information see: https://docs.godotengine.org/en/latest/tutorials/shaders/advanced_postprocessing.html#depth-texture

BenjaminNavarro commented 2 years ago

Thanks @clayjohn, I didn't know about this difference between OpenGL and Vulkan. I made the change you suggested, it improved the situation but the result is still quite different from the GLES3 version. Here are the two versions again for comparison:

GLES3: low_poly_water_3_4_v2

Vulkan: low_poly_water_4_0_v2

We can even see in the inspector pane how the two shaders look different even though their settings are the same. Would it be another thing handled differently between OpenGL and Vulkan? Or something more on the Godot side of things?

I attach the updated 4.0 project LowPolyWaterShader_4_0_v2.zip

Calinou commented 2 years ago

The different water color looks like a sRGB/linear color conversion issue.

bruce965 commented 2 years ago

I have a simpler case with similar results.

On Godot v3.4 with GLES3 it looks like this:

image

shader_type canvas_item;

uniform vec4 color : hint_color;

void fragment() {
    COLOR = color;
}

test_godot3.zip


On Godot 4.0-alpha13 with Vulkan it looks like this:

image

shader_type canvas_item;

uniform vec4 color : source_color;

void fragment() {
    COLOR = color;
}

test_godot4.zip


Definitely looks like a sRGB/linear color conversion issue, probably from the uniform, because with the following shader code the color looks correct even on Godot 4.0-alpha13.

shader_type canvas_item;

void fragment() {
    COLOR = vec4(0., .7, 1., 1.);
}

image

TokisanGames commented 2 years ago

Here are some slow, medium, and fast linear/srgb conversions for you. Accuracy is inversely correlated to speed. See if these will fix your color issues.

vec4 toLinearFast(vec4 col) {
    return vec4(col.rgb*col.rgb, col.a);
}

vec4 toSRGBFast(vec4 col) {
    return vec4(sqrt(col.rgb), col.a);
}

vec4 toLinearMed(vec4 col) {
    return vec4(pow(col.rgb, vec3(2.2)), col.a);
}

vec4 toSRGBMed(vec4 col) {
    return vec4(pow(col.rgb, vec3(.4545)), col.a);
}

vec4 toLinearSlow(vec4 col) {
    bvec4 cutoff = lessThan(col, vec4(0.04045));
    vec4 higher = vec4(pow((col.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)), col.a);
    vec4 lower = vec4(col.rgb/vec3(12.92), col.a);
    return mix(higher, lower, cutoff);
}

vec4 toSRGBSlow(vec4 col) {
    bvec4 cutoff = lessThan(col, vec4(0.0031308));
    vec4 higher = vec4(vec3(1.055)*pow(col.rgb, vec3(1.0/2.4)) - vec3(0.055), col.a);
    vec4 lower = vec4(col.rgb * vec3(12.92), col.a);
    return mix(higher, lower, cutoff);
}
BenjaminNavarro commented 2 years ago

Ok so it seems that converting the color uniform parameter to sRGB gives the proper color. It makes the color consistent between setting it from the inspector and hardcoding it in the shader as well as giving the same color as with 3.x.

So doing this conversion internally would solve the color issue and @bruce965 problem but on my side there is more to it. The water tint changes a little but it still looks very different.

After some testing I realized that in 4.0 the water color is unaffected by the DirectionalLight, toggling it on and off only changes the other objects color (white vs gray) while doing the same thing with 3.x has a clear impact on both the water and the objects.

Also, with 4.0, bumping the ground energy in the ProceduralSkyMaterial to ~50 gives the water a very similar look as with 3.x, but now the other objects are way too bright. But doing the same in 3.x doesn't change the water color, it only gives it brighter reflections.

Finally, I replaced the water mesh+shader by a simple MeshInstance3D with a StandardMaterial3D with the same color and roughly the same transparency and now the "water" plane looks and behave as expected.

So there seems to be something wrong regarding the lighting of objects with custom shaders as materials...

BenjaminNavarro commented 2 years ago

Just for info, I just tested with the current master branch (dc4b616596) and the issue is still there

BenjaminNavarro commented 1 year ago

Ok so I found a fix but I don't know if it's an expected change in behavior between GLES3 and Vulkan or a bug.

In my fragment shader I had the following line:

NORMAL = normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));

But if I negate the result of the normalize function I get the lighting to work properly and a very similar look between GLES3 and Vulkan:

NORMAL = -normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));

So either there is a problem in the output sign of one of the function involved or normals have opposite signs between the 2 renderers.

Would you guys have any idea of which is more likely ?