0 votes

Hi, I followed this tutorial for an awesome looking low-poly water for a simple looking Android 3D game.

Unfortunately the code needs GLES3 partial derivative functions like dFdX() and dFdy(), which crashes when deployed on my Oneplu5 (Android 7.1.1).
Only GLES2 runs on my device, which means, no access to partial derivatives...

It seems OpenGL2 can run extensions including the one containing dFdx() and dFdy().
I see people using this define to enable the extension

#define GL_OES_standard_derivatives 1 

Is there any way to either

  • Use those extensions in Godot shading language

    • I tried using uniform, no success
  • Calculate dFdx() and dFdy() in software (even if it's inefficient)

    • I came across a formula that goes like :

      dFdx(p(x,y)=p(x+1,y)-p(x,y)
      

      But it is 2D and I don't really know which how if what p() mathematical function we're talking about

  • Get the Vertex Normal in another way ?

I have a lot of C++ experience but not in OpenGL and not in Godot shading

asked Feb 25 in Engine by julliansana (12 points)

1 Answer

0 votes

Godot shading language is converted to GLSL but it doesn't support the # directives at all unfortunately. Enabling an extension would require to modify VisualServer rasterizer_gles2.cpp I think.

I had a quick look at the video and it would seem derivatives were an optimized way to get normals (I personally didn't know it worked like that).
To do that manually, you can compute them in the vertex shader, using the same function you use to displace the water (same technique as heightmaps). Take the height at the vertex, then the height from neighbor positions in negative X and negative Z. From these 3 points you can compute the normal with a cross product.
You can also put your height calculation in a function to make this easier.

answered Feb 25 by Zylann (26,033 points)

Wow, thanks for answering that fast !

I guess, modifying the cpp file would mean re-compiling Godot right ? If so, I'll just try to calculate them as you said.

I'm not sure how to access the VERTEX neighbors though. I see in this tutorial that it's done through a heightmap with the following :

pos=VERTEX.xz
NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, 
                    k - height(pos + vec2(0.0, 0.1), TIME)));

I don't have a height map, so I replaced "height" by "apply_distorsion":

v_step = 1/mesh_size //It's a square
vec3 vx= applyDistortion(VERTEX-vec3(v_step,0,0),0.1); //-x neighbor
vec3 vz= applyDistortion(VERTEX-vec3(0,0,v_step),0.1); //-z neighbor
VERTEX= applyDistortion(VERTEX,0.1);

I'm not sure if that was what you meant when you said -x and -z neighbor

Then I tried different ways of calculating the NORMAL, but I must admit I'm shooting in the dark here :

A)

NORMAL=normalize(cross((VERTEX-vx),(VERTEX-vz)));

B)

NORMAL=normalize(cross((VERTEX-vec3(0,vx.y,0)),(VERTEX-vec3(0,vz.y,0))));

It runs, but it's giving me the wrong results. I'm sure there is something simple I'm not getting.

EDIT :

I've been at it the whole night after work and I can't seem to come close to what the derivatives do. I would eally appreciate some help.

Here's the shader code, it's applied to a PlaneMesh subdivided 10x10y of size 10x10
I pretty muched tried to interchange everything with everything :

vX<-->vY<-->vZ<-->(VERTEX-vX)[...] with division by pt_size or not etc...

shader_type spatial;
uniform int GL_OES_standard_derivatives;

const float v_step = 10.0/1.0;
uniform vec4 out_color : hint_color =vec4(0.0,0.2,1.0,1.0);
uniform float distortion_amount : hint_range(0.2,1.5)=1.5;
uniform float distorsion_speed : hint_range(0.2,5)=1.0; 
uniform float distorsion_seed : hint_range(0,1000)=1.0; 

float generate_offset(float x, float z, float val1, float val2, float time){
    float speed=distorsion_speed,amount=distortion_amount;

    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*(x*z+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 amount){
    float xd = 0.0;//generate_offset(vertex.x,vertex.z,0.2,0.1,amount);
    float yd = generate_offset(vertex.x,vertex.z,0.1,0.3,amount);
    float zd = 0.0;//generate_offset(vertex.x,vertex.z,0.15,0.2,amount);

    return vertex + vec3(xd,yd,zd);
}

varying vec3 preV;
varying vec3 vx;
varying vec3 vy;
varying vec3 vz;
varying float t;
varying vec3 normall;
varying float pt_size;

void vertex(){
    //prevVertex = VERTEX;

    t=TIME;
    pt_size=POINT_SIZE;
    preV=VERTEX;
    VERTEX= applyDistortion(VERTEX,t*0.1);
    vx= applyDistortion(VERTEX+vec3(POINT_SIZE,0,0),t*0.1);
    vy= applyDistortion(VERTEX+vec3(0,POINT_SIZE,0),t*0.1);
    vz= applyDistortion(VERTEX+vec3(0,0,POINT_SIZE),t*0.1);

}

void fragment(){

    //NORMAL=normalize(cross(dFdx(VERTEX),dFdy(VERTEX)));
    vec3 vX= applyDistortion(VERTEX-vec3(pt_size,0,0),t*0.1);
    vec3 vY= applyDistortion(VERTEX-vec3(0,pt_size,0),t*0.1);
    vec3 vZ= applyDistortion(VERTEX-vec3(0,0,pt_size),t*0.1);

    NORMAL=normalize(cross((VERTEX-vX)/pt_size,(VERTEX-vY)/pt_size));


    METALLIC =0.0;
    SPECULAR=0.0;
    ROUGHNESS=0.2;
    ALBEDO= out_color.xyz;
}

I gave it a go myself to recall what it takes in details. I briefly looked at your shader but I wasn't sure if your displacement function was actually derivable (i.e I don't know if it's actually as continuous as it looks, since deriving it means taking close enough samples... but how close?).

I also found the doc links to this video, which explains it: https://youtu.be/vm9Sdvhq6ho?t=265

On my own with a unit-sized plane subdivided 10 times, I had this more or less working (not optimized, just fiddling result):

shader_type spatial;

uniform vec4 out_color : hint_color =vec4(0.0,0.2,1.0,1.0);

varying flat vec3 v_normal;

float get_wave(vec2 pos, float time, vec2 dir, float period, float amplitude) {
    return amplitude * sin((pos.x * dir.x + pos.y * dir.y + time) / period);
}

float get_height(vec2 pos, float time) {
    time *= 0.3;
    return get_wave(pos, time, vec2(0.5, 0.5), 0.3, 0.1)
        + get_wave(pos, time, vec2(-0.6, 0.4), 0.12, 0.1)
        + get_wave(pos, time, vec2(0.7, -0.2), 0.5, 0.06)
        + get_wave(pos, time, vec2(0.9, 0.1), 0.3, 0.03);
}

void vertex() {
    float side_step = 0.01;
    float h = get_height(VERTEX.xz, TIME);
    float h_right = get_height(VERTEX.xz + vec2(side_step, 0.0), TIME);
    float h_up = get_height(VERTEX.xz + vec2(0.0, side_step), TIME);
    float dx = (h_right - h) / side_step;
    float dz = (h_up - h) / side_step;
    vec3 pos_right = vec3(1, dx, 0);
    vec3 pos_up = vec3(0, dz, 1);
    v_normal = -cross(normalize(pos_right), normalize(pos_up));
    NORMAL = v_normal;
    VERTEX.y = h;
}

void fragment(){
    NORMAL = (INV_CAMERA_MATRIX * (vec4(v_normal, 0.0))).xyz;
}

It doesn't look "triangulish" though, I'm not sure why.

Hey, thanks a lot,

In short :

That looks great to me.
I can't make it work in GLSL2 though, and the game crashes on Android if I select GLSL3.
GLSL2 doesn't crash, but it's not flat shaded, it's more continuous and it looks off.
Do you have any clue why ? What happens if you run this shader on GLSL2 ?

(Below is just more details about my previous mistakes)

I adapted it to my code and it didn't work with the original function, probably because :

  • mod() breaks continuity and the function
  • the apply_distorsion() was changing the vertex X,Y, and Z at the same time
  • the function actually not mapping, but incrementing/decrementing the vertex position "y+=f(x,z)"

But changed the distorsion to your function and it worked ... until I changed to GLES2, and then it lost the flat shading.
The flat specifier in fromt of v_normal is not changing anything wheter it's tehre r not.

There's a lot that I was missing.

I would have never thought to put the 1s in here

vec3 pos_right = vec3(1, dx, 0);

The flat is also completely new to me, but I looked it u, it seems obvious now

varying flat vec3 v_normal;

I tried without INVCAMERAMATRIX to see the difference but I couldn't see any.

NORMAL = (INV_CAMERA_MATRIX * (vec4(v_normal, 0.0))).xyz;

Thanks for your help again.

When I try my shader in GLES2 it doesn't crash, however the flat keyword had zero effect. I have no idea if that's not supported in this renderer, or if it's a Godot bug. Looks more likely to be not supported in GLES2.

So let's change strategy.
Don't use PlaneMesh. It looks smooth in GLES2 because vertices are shared, the geometry needs to be made such that each triangle is independent. Maybe you can make it in Blender? Or you could generate it from a script.

I have no idea why it crashes though. Does it crash without the flat?

I used INV_CAMERA_MATRIX to transform the normal because otherwise the shading was changing depending on the angle I was looking at the plane. Also I'm assigning it in the fragment shader because if I don't, the flat keyword would have no effect to make faces look flat. But since GLES2 doesnt supports that, you won't even need to write a fragment part anyways.

Yeah, I think generating is the way forward.

It's probably going to be easier to manipulate the vertices, as I won't be limited by shader loop logic...

I actually tried to import it from an animated blender model at first, but I have lighting problems when importing flat shaded models even with the better collada exporter.
I had partial success with .obj until I scaled it. I should crete another post for that.

  • "I have no idea why it crashes though. Does it crash without the flat?"
    It only crashes on android if I select GLES3, GLES2 runs fine on android (and HTML), the flat look is just not there.
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.