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.
1.2. Amplitude parameters
Increase the amplitude parameter _Amplitude, mainly for peak control.
p.y = _Amplitude * sin(p.x);
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);
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));
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.
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.
//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.
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 .
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.
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
}
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.
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));
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.
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.
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);
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,
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);
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.)
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)
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.
)
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));
}
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.
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));
//变换,阴影计算等
}
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.
single wave for comparison