[unity shader] water body rendering basics - vertex wave

1. About the vertex waveform:

In order to represent the water body change in a wide water area, it is often necessary to carry out the overall movement change of the water level. That is, the vertices of the plane are displaced to achieve the undulating effect of waves. Now for the composition of waves, such as fast Fourier transform and statistical theory of waves, the application in the game is relatively perfect. Today I mainly do a basic wave implementation: sine wave.

1.1. Basic sine wave

We drag out a plane and modify its vertex shader. In the fragment shader, we directly return a sea surface color.

v2f o;
float3 p;
p = v.vertex;
p.y = sin(p.x);
//注意这里肯定不能在视口变换完后再求正弦,原因不用多说了吧?
o.vertex = UnityObjectToClipPos(p);

Get the basic waveform.
insert image description here

1.2. Amplitude parameters

Increase the amplitude parameter _Amplitude, mainly for peak control.

p.y = _Amplitude * sin(p.x);

insert image description here

1.3. Wavelength parameters

The wavelength parameter _Wavelength mainly affects the period of the sine function. That is to say, the function takes longer to complete a periodic change, and the performance meaning is the width of the water wave.
Note that the period of the sine function sin(x) is 2π by default. In order to control it more intuitively, we need to add a k parameter as the final control vector. Solve k by 2π/_Wavelength, and realize that the larger _Wavelength is, the greater the width of the water wave in the final performance.

float k = 2 * UNITY_PI / _Wavelength;
p.y = _Amplitude * sin(k * p.x);

insert image description here
Pay attention to the obvious problem of sharp jitter here, indicating that the number of vertices of the plane itself is obviously not enough to support too detailed performance. In the next step, we redo the two planes with more vertices for comparison.

1.4. Wave speed parameters

_Wavespeed, this does not need much explanation

p.y = _Amplitude * sin(k * (p.x + _Wavespeed * _Time.y));

insert image description here
The first one here is the plane that comes with unity, and the left and right planes are 20 20 and 30 30 planes made by myself . You can see that when the wavelength is small, there will be hard edges due to insufficient vertices.
insert image description here
Tips: When the blender model is exported to
the default configuration of unity, the size difference is five times, and it is maintained

1.5. Find the normal vector

The current problem is that even with the waveform, the plane looks like just a layer of skin, so normal vectors are needed to facilitate lighting calculations in the fragment shader.
Since we have adjusted the vertices here, the original normal information of the plane must not be available, and we have to solve it ourselves.
First of all, we need to solve his tangent direction, which is to find the derivative.
Fortunately, currently our vector only changes in the x and y directions, that is, the derivative in the z direction is 0. Derivative T = ( x', y' , 0 ).
Since the current change is x, the final derivative is derived with respect to x, T = ( 1, k * _Amplitude * cos( k * x), 0).
For the direction of the vice tangent, we are very sure, it is the unit vector (0, 0, 1) along the z direction.
Finally, we obtain the normal vector by cross product.
insert image description here

//in vertex shader
float3 tangent = normalize(float3(1, k * _Amplitude * cos(f), 0));
float3 normal = cross(float3(0, 0, 1.0), tangent);
//normal直接传递使用即可

//in vertex shader
float3 LightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0 * dot(i.normal, LightDir);

We use the custom water surface color as ambient, add it to diffuse and return it.
insert image description here

1.6. About grid precision

When the wavelength is relatively large, because the amplitude of the water wave itself is large, that is, the relative displacement between vertices is relatively small, the performance impact of the difference in the plane mesh itself seems to be little different. When the wavelength is small, the difference in precision between meshes can have a significant impact on performance.
When the wavelength is 2, the grid performance difference of 10 10, 20 20, 30 30 is particularly obvious. So here we introduce a high-precision plane grid of 100 100, and compare the grids of 20 20, 30 30, and 100 100. Only 100 by 100 planes will be used in our subsequent cases .
insert image description here

insert image description here

1.7. Shadow correction

We directly apply the shadow three-piece set to add shadows to the sine wave. It is obvious that there are some problems with the default shadow calculation: although the calculation of the normal is correct, the change of the vertices obviously does not affect the shadow map, resulting in shadows. It looks very flat. (The visual effect here has changed, I just adjusted the lighting parameters again)
The main reason is that the default shadowmapping method does not support the shader of vertex offset, so we need to write a shadowcaster ourselves.
insert image description here
For the surface shader, we have a relatively simple solution, let the shadow calculation use our vertex shader for vertex changes.

#pragma surface surf Standard fullforwardshadows vertex:vert addshadow
//#pragma surface <surface function> <lighting model> <optional parameters>

But for the unlit shader, it does not support directly using #pragma to specify the vertex shader, we need to write a shadow calculation pass by ourselves.
In fact, shadowcaster also has three sets:
V2F_SHADOW_CASTER defined in the fragment structure definition;
TRANSFER_SHADOW_CASTER_NORMALOFFSET( ) placed in the vertex shader for optical port transformation;
SHADOW_CASTER_FRAGMENT(i) placed in the fragment shader;

Pass
{
    
    
    //原本的顶点偏移pass,记得添加阴影三件套
}

Pass
{
    
    
    Tags {
    
    "LightMode"="ShadowCaster"}

    CGPROGRAM

    #pragma vertex vert 
    #pragma fragment frag
    #pragma multi_compile_shadowcaster

    #include "UnityCG.cginc"

    float _Amplitude;
    float _Wavelength, _Wavespeed;
    //阴影映射不需要颜色纹理相关的参数

    struct v2f
    {
    
    
        V2F_SHADOW_CASTER;
    };

    v2f vert(appdata_base v)
    {
    
    
        v2f o;
        float3 p;
        float k = 2 * UNITY_PI / _Wavelength;
        p = v.vertex;
        float f = k * (p.x + _Wavespeed * _Time.y);
        p.y = _Amplitude * sin(f);
        
        v.vertex.xyz = p;
        //无需进行视口变换,只需要给原有的vertex赋值
        
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
        
        return o;
    }

    fixed4 frag(v2f i): SV_Target
    {
    
    
    	//若需要做深度剔除,仍需要进行单独计算处理
        SHADOW_CASTER_FRAGMENT(i);
    }

    ENDCG
}

insert image description here
When the vertex offset calculation method is changed, the vertex calculation method in shadowcaster should also be adjusted accordingly. For the sake of simplification, I will not give additional instructions on the shadowcaster pass separately in the future.

Gerstner波:

A sine wave is simpler, but that's obviously a bit of an oversimplification. So we need to go further on this basis.
Similarly, the vertex shader we made actually processes the vertices (a layer of skin) on the surface of the model. In a sine wave, each vertex follows a sine function and only moves up and down in the y-axis direction. In Gerstner waves, the vertex moves along the x-axis while moving along the y-axis.
insert image description here
In the direction of the x-axis, you can see that the offset of the vertex changes from positive to negative and then to positive, which is a characteristic of a typical cosine function.
So our new vertex equation is to add a cosine function as an offset on the basis of the original x value:
vertex = (x + A cos(k x), A sin(k x), 0)
after derivation, that is,
T = (1 - A k sin(k x), A k cos(k x), 0)

The minor tangent remains unchanged, and we can directly use the cross product to solve for the normal.

p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

float3 tangent = normalize(float3(1 - k * _Amplitude * sin(f), k * _Amplitude * cos(f), 0));
float3 normal = normalize(cross(float3(0, 0, 1.0), tangent));

insert image description here

2.1. Adjustment of overlapping waves

While the resulting waves may look good, that's not always the case. For example, reducing both the wavelength and the amplitude produced strange results in which the individual waves appeared to fold into each other.
insert image description here
insert image description here
Why is there such a phenomenon? It is mainly due to the adjustment of the calculation method of the x value, coupled with the combined effect of the two parameters of amplitude and wavelength, resulting in overlapping results of different x calculation values.
insert image description here
Why is there such a situation?
Mainly under the influence of the two parameters, the value of x2-x1 cannot be guaranteed to be greater than the value of A*( cos(k x1) - cos(k x2)).
When k is large, the trigonometric function will be greatly compressed, and the range of change within a certain range will be very large, resulting in the maximum value of A*( cos(k x1) - cos(k x2)) being 2A.
Obviously, the A value is an uncontrollable factor. In order to better control this situation, we use 1/k to replace the original A (amplitude) parameter. As k increases, resulting in large swings in the trigonometric functions, 1/k decreases accordingly, cutting the maximum value of ( cos(k x1) - cos(k x2)) to a maximum value of 2/k. In addition, we add a _Steepness parameter and use _Steepness /k to replace the original amplitude parameter to better control the peak.

float _Amplitude = _Steepness / k;
float f = k * (p.x + _Wavespeed * _Time.y);
p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

insert image description here
It can be seen that after using the new parameters, the phenomenon of peak overlap is greatly alleviated. After further reducing _Steepness, the entire peak will be correspondingly flattened.

2.2. Wave speed in reality

In real physics, the wave speed is actually related to the wavelength, that is,
insert image description here
we don’t talk too much about physical issues, and just replace the parameters directly, so we won’t need _Wavespeed in the future.

float _Amplitude = _Steepness / k;
//新增c,波长相关的波速参数,原有的_Wavespeed被替换
float c = sqrt(9.8 / k);
float f = k * (p.x + c * _Time.y);
p.y = _Amplitude * sin(f);
p.x += _Amplitude * cos(f);

insert image description here

2.3. Self-defined direction wave

After talking about the wave formula with the x-axis as the direction and any straight line in the xz plane, it is ready to come out. That is, x and z will act on the calculation results in the y direction at the same time, and the x and z directions will also be offset according to the current parameter value.
For **two-dimensional direction (x1, z1)**:
the f value first put into the trigonometric function should become,

f =  k *(dot((x1, z1),(vertex.x,vertex.z)+ c * _Time.y)

Therefore, in the offset vertex Vo, the values ​​of x, y, and z are:
x = x + x1 * _Steepness / k * cos(f)
y = _Steepness / k * sin(f)
z = z + z1 * _Steepness / k * cos(f)

Now that the 3D representation of the vertex offset is done, it's time to solve for the normal vector. It can be seen that with the participation of self-defined direction waves, the x-axis and z-axis will all participate in the solution of the normal.
There are two common solutions. One is to solve the tangent and vicetangent along the direction of the wave, and the other is to solve according to the x and z directions respectively. (Forgive me that the normals shown in the illustration are not strictly accurate.)
insert image description here
It is obvious that the solution of tangents along the wave direction is basically impossible, or the complexity is frighteningly high. The solution of solving the respective partial derivatives along the x and z directions is relatively low in complexity. Because when only one normal line is given, the result of cross multiplying its two vertical tangents is not unique.
Carry out dx derivative along the x direction to get tangent:
x = 1 - x1 * x1 * _Steepness * sin(f)
y = x1 * _Steepness * cos(f)
z = - z1 * x1 * _Steepness * sin(f)

Perform dz derivation along the z direction to get the bitangent:
x = - x1 * z1 * _Steepness * sin(f)
y = z1 * _Steepness * cos(f)
z = 1 - z1 * z1 * _Steepness * sin(f)

insert image description here

2.4. Multi-wave superposition

In a real sea scene, multiple waves are often superimposed, and each wave can have different parameters. Under the original setting, the three parameters are very inconvenient to manage.
We simply use a four-dimensional variable to integrate it together, and use a function to process this four-dimensional variable.
insert image description here)

Properties
    {
    
    
    	//其他属性
		_WavwA("wave A: directionX, directionZ, _Steepness, _Wavelength", Vector) = (1, 1, 0.8, 3.0)
	}
float3 gerstner(float4 wave, float3 p, inout float3 tangent, inout float3 bitangent)
{
    
    
	///另附
}
v2f vert (appdata v)
     {
    
    
         v2f o;
         float3 p = v.vertex;
         float3 tangent = float3(0.0, 0.0, 0.0);
         float3 bitangent = float3(0.0, 0.0, 0.0);
         
         p += gerstner(_WavwA, v.vertex, tangent, bitangent);

         float3 normal = normalize(cross(bitangent, tangent));
         
         //其他变换和属性赋值,阴影计算
         
         return o;
     }

The main thing is to move the original calculation to the custom function

float3 gerstner(float4 wave, float3 p, inout float3 tangent, inout float3 bitangent)
{
    
    
    float k = 2 * UNITY_PI / wave.w;
    float _Amplitude = wave.z / k;
    float c = sqrt(9.8 / k);
    float f = k * ( dot(wave.xy, p.xz) + c * _Time.y);

    tangent += normalize(float3(1 - wave.x * wave.x * k * _Amplitude * sin(f), 
                                        wave.x * k * _Amplitude * cos(f), 
                                        - wave.x * wave.y * k * _Amplitude * sin(f)));

    bitangent += normalize(float3( - wave.y * wave.x * k * _Amplitude * sin(f), 
                                        wave.y * k * _Amplitude * cos(f), 
                                        1 - wave.y * wave.y * k * _Amplitude * sin(f)));
    
    return float3( wave.x * _Amplitude * cos(f), _Amplitude * sin(f), wave.y * _Amplitude * cos(f));
}

insert image description here
After completing the pre-preparation, we directly define two waves in the attribute and add them in the vertex shader to complete the multi-wave superposition.
insert image description here

v2f vert (appdata v)
{
    
    
    v2f o;
    float3 p = v.vertex;
    float3 tangent = float3(0.0, 0.0, 0.0);
    float3 bitangent = float3(0.0, 0.0, 0.0);
    
    p += gerstner(_WavwA, v.vertex, tangent, bitangent);
    p += gerstner(_WavwB, v.vertex, tangent, bitangent);

    float3 normal = normalize(cross(bitangent, tangent));
    
    //变换,阴影计算等
}

insert image description here
It should be noted that after multiple waves are superimposed, the problem of overlapping waves will reappear. We need to pay attention to the cumulative sum of each wave_Steepness not exceeding 1 to avoid this problem.

2.4.1. Three-wave superposition

The parameters are as follows. Generally, the three waves will adopt equal proportions of wavelengths.
insert image description here
insert image description here
single wave for comparison
insert image description here

Guess you like

Origin blog.csdn.net/misaka12807/article/details/131263570