Process vertices -- add a water surface in a 3D world

Source: Internet
Author: User
Tags eye vector
Problem

You can calculate all the 3D positions of a wave on the CPU, but this consumes a lot of resources. It is unacceptable to send a large amount of data to the video card at each frame.

Solution

In the XNA program, you only need to create a plane mesh composed of triangles. This is similar to creating a terrain. It has been explained in the tutorial 5-8, but this time the grid is flat and you will need to transfer the data to the video card at a time. When drawing a grid, vertex shader adds water waves to the grid, and pixel shader adds reflection, which is done in the GPU. The CPU only processes the Draw command, enables the CPU to handle more important tasks.

Vertex shader accepts vertex data from the mesh and changes their height so that the flat mesh forms a choppy sea. You can use a sine wave to generate this height. Only one sine wave will make the sea surface look too idealistic, because every wave is the same.

Fortunately, operations on the GPU can be executed four times in parallel, so the time used to calculate a sine wave is the same as that used to compute the four sine waves. Vertex shader calculates four sine waves and sums them, as shown in Figure 5-32. When you set different wavelengths, velocity, and amplitude for each wave, a relatively real sea surface will be formed.

Figure 5-32 superposition of four sine waves

Only when the sea surface reflects the surrounding environment will it look real enough. In pixel shader, you will sample the reflection color from the empty box (see tutorial 2-8. However, if the reflection is too perfect, it will form a water surface like glass, but it is not true. So in each pixel, you need to add a concave-convex ing on the water surface (see tutorial 5-16), and then add some small pixel-by-pixel attention on the large waves.

The final result is adjusted using the fresh item, so that the pixel shader is interpolated between the dark blue and the reflected color based on the angle of view.

Working Principle

In the XNA project, you need to import and draw a sky box (see tutorial 2-8 ). next, you need to generate vertices and indexes for the flat mesh. This is only a simplified version of the terrain generated in the 5-8 tutorial. Use the following code to generate a vertex:

private VertexPositionTexture[] CreateWaterVertices() {    VertexPositionTexture[] waterVertices = new VertexPositionTexture[waterWidth * waterHeight];         int i = 0;     for (int z = 0;z < waterHeight; z++)     {        for (int x = 0; x < waterWidth; x++)         {             Vector3 position = new Vector3(x, 0, -z);             Vector2 texCoord = new Vector2((float)x / 30.0f, (float)z / 30.0f);             waterVertices[i++] = new VertexPositionTexture(position, texCoord);         }    }    return waterVertices;}

The grid is flat, because all Y values are 0. Only 3D positions and texture coordinates are stored in the vertex. The indexing method has been described in 5-8 of this tutorial. After the vertex and index are defined, you are ready to write the HLSL effect file.

XNA-to-HLSL variable

You want to make the generation of the sea as flexible as possible, so you expect to be able to set many variables in the XNA project. Because the 3D location you are processing is converted to 2D screen coordinates, you need the World, View, and Projection matrices. Then you want to completely control the four sine waves. Each wave has four controllable variables: amplitude, wavelength, velocity, and direction.

Because you want vertex shader to update a wave at each frame, you need to know the current time. To add a water surface reflection, you need to know the 3D position of the camera. Finally, you need to add a Paster texture to the water surface. You also need a Paster texture and two variables to set the intensity and size of the Paster texture:

float4x4 xWorld; float4x4 xView; float4x4 xProjection; float4 xWaveSpeeds; float4 xWaveHeights; float4 xWaveLengths; float2 xWaveDir0; float2 xWaveDir1; float2 xWaveDir2; float2 xWaveDir3; float3 xCameraPos; float xBumpStrength; float xTexStretch; float xTime; 

You need a color for sampling and reflection in the sky box and a concave and convex texture:

Texture xCubeMap; samplerCUBE CubeMapSampler =sampler_state{    texture = <xCubeMap> ;    magfilter = LINEAR;     minfilter = LINEAR;     mipfilter=LINEAR;     AddressU =mirror;     AddressV = mirror; }; Texture xBumpMap; sampler BumpMapSampler =sampler_state {    texture = <xBumpMap> ;     magfilter = LINEAR;     minfilter = LINEAR;     mipfilter=LINEAR;     AddressU = mirror;     AddressV = mirror; }; 
Output Structure

To query the reflected color, pixel shader needs to know the camera position. The Tangent-to-World matrix needs to be constructed based on the processing of the concave and convex textures in pixel shader.

Pixel shader only needs to calculate the color of each pixel:

struct OWVertexToPixel{    float4 Position : POSITION;     float2 TexCoord : TEXCOORD0;     float3 Pos3D: TEXCOORD1;     float3x3 TTW: TEXCOORD2; };struct OWPixelToFrame{    float4 Color : COLOR0; }; 
Vertex Shader: sine wave

The most important task of vertex shader is to adjust the height of each vertex. As we have explained above, you have superimposed the four sine functions to generate water waves. A sine function requires a parameter. When this parameter is added, a waveform is formed between-1 and 1. You use the vertex position as a parameter of the sine function. This parameter is related to the direction of wave propagation. You can multiply the X and Z positions of the vertex and the point in the propagation direction. For all vertices perpendicular to the propagation direction of the wave, the point multiplication value is the same. If the parameters are the same, the sine functions are the same, so their heights are the same. This improves the waveform of the wave in the correct place.

OWVertexToPixel OWVertexShader(float4 inPos: POSITION0, float2 inTexCoord: TEXCOORD0){    OWVertexToPixel Output = (OWVertexToPixel)0;         float4 dotProducts;     dotProducts.x = dot(xWaveDir0, inPos.xz);     dotProducts.y = dot(xWaveDir1, inPos.xz);     dotProducts.z = dot(xWaveDir2, inPos.xz);     dotProducts.w = dot(xWaveDir3, inPos.xz); } 

Because shader allows you to sum up the four sine functions, You need to perform four point multiplication on the XZ coordinate and wave direction of the vertex. Before using these vertices as parameters of the sine function, you need to divide them by the xWaveLengths variable to adjust the wavelength.

The current time must be added to the parameter to move the water wave. Multiply the xWaveSpeeds variable by the current time, allowing you to define the velocity of each wave, so that some waves can be faster or slower than other waves. The following code is used:

float4 arguments = dotProducts/xWaveLengths+xTime*xWaveSpeeds; float4 heights = xWaveHeights*sin(arguments); 

The result of the sine function is multiplied by the xWaveHeights variable, so that you can scale the four waveforms independently. Remember that this code is executed four times in parallel, so that you can quickly obtain the height of the sine wave of the current vertex at the current time.

After calculating the sine function, add them as the Y coordinates of the vertex:

float4 final3DPos = inPos; final3DPos.y += heights.x; final3DPos.y += heights.y; final3DPos.y += heights.z; final3DPos.y += heights.w; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(final3DPos, preWorldViewProjection); float4 final3DPosW = mul(final3DPos, xWorld); Output.Pos3D = final3DPosW; 

This can effectively increase the vertex height. Once you know the final position of the vertex, you can convert them into 2D screen positions.

Pixel shader also needs to know the 3D location, so you need to pass the 3D location information to pixel shader (because Output. Position is not processed in pixel shader ).

Pixel Shader: the simplest form

After vertex shader converts the 3D position of vertices, this simple pixel shader applies the same color to each pixel and draws them to the screen:

OWPixelToFrame OWPixelShader(OWVertexToPixel PSIn) : COLOR0{    OWPixelToFrame Output = (OWPixelToFrame)0;     float4 waterColor = float4(0,0.1,0.3,1);     Output.Color = waterColor;     return Output; } 

Only one piece of blue is mixed up and down

Define Technique

Add the technique definition to make effect available. If you update the xTime variable per frame, the wave will also be updated in vertex shader:

technique OceanWater{    pass Pass0    {        VertexShader = compile vs_2_0 OWVertexShader();         PixelShader = compile ps_2_0 OWPixelShader();     }} 
Vertex Shader: normal Calculation

All Ray calculations use the vertex's normal. In your XNA project, you do not need to add the vertex's normal information. The normal vector must be perpendicular to the surface, because vertex shader constantly changes the surface, so it also needs to calculate the normal in vertex shader.

For a plane, the direction of the normal is (0, 1, 0) up. When a water wave passes through a plane, you need to know how much the (0, 1, 0) vector will deviate. You can evaluate the offset of any point in a function (such as a sine function. It sounds hard, but fortunately the derivative of the sine function is the cosine function. If it is too abstract, You can see 5-33. The solid line represents the sine function. You can see that the dotted line shows the degree of normal offset of any point in the waveform very well (in fact it is perfect.

Figure 5-33 sine functions (solid line) and Cosine Functions (dotted line)

For example, in the peak and valley (where the water surface is flat), the cosine value is 0, which means that the (, 0) vector does not need to be changed, and the cosine function at the equilibrium position of the wave is the largest.

This means that after the sine function is calculated, the height function is also required for derivation. Because the height function needs to be multiplied by xWaveHeights, the evaluation function must also be multiplied by xWaveHeights. The derivative of the sine function is the cosine function. When dividing dotProducts (horizontal axis in Figure 5-33) by xWaveLengths, it is also divided by xWaveLengths.

Float4 derivatives = xWaveHeights * cos (arguments)/xWaveLengths;

The above formula indicates that the higher the wave, the steep the waveform, the larger the normal offset, and the longer the wavelength, the less the normal deviation (0, 1, 0.

Note:The sine and Cosine are multiplied by xWaveHeights, which means that if you set the height to 0, the vertices will not fluctuate or the normal will not deviate.

Now you have calculated the derivatives and added them to obtain the normal offset of the current vertex. The wave in Figure 5-33 is 2D, so the cosine value represents the offset of the line forward or backward. And your wave is in 3D space, so in the above words, you should replace "wave propagation direction" with "Forward ":

float2 deviations = 0; deviations += derivatives.x*xWaveDir0; deviations += derivatives.y*xWaveDir1; deviations += derivatives.z*xWaveDir2; deviations += derivatives.w*xWaveDir3; float3 Normal = float3(-deviations.x, 1, -deviations.y); 

The vertex normal is (0, 1, 0), indicating that there is almost no deviation, corresponding to the peak and valley of the vertex, if the wavelength is very short, the normal almost all point to the wave propagation direction.

Vertex Shader: Create the Tangent-to-World Matrix

When you use the normal water supply area generated in the previous section to add light, the light and shade effect is good, but you also need to add some small fluctuations on the water surface. To add this details, you need to use the concave-convex ing. You need to generate the correct Tangent-to-World matrix before performing the concave/convex ing in pixel shader.

As explained in tutorial 5-16, the rows in this matrix correspond to the normal, subnormal, and tangent vectors. Now that you have a normal vector, continue to define the tangent and subnormal vectors:

float3 Binormal = float3(1, deviations.x, 0); float3 Tangent = float3(0, deviations.y, 1); 

These three vectors must be perpendicular to each other. Because you want to make the normal deviation (0, 1, 0) direction, so you need to set the sub-normal deviation (, 0) Direction, tangent deviation (, 1) Direction.

After knowing the three vectors in the Tangent space, it is easy to define the Tangent-to-World matrix, as explained in the tutorial 5-16:

float3x3 tangentToObject; tangentToObject[0] = normalize(Binormal); tangentToObject[1] = normalize(Tangent); tangentToObject[2] = normalize(Normal); float3x3 tangentToWorld = mul(tangentToObject, xWorld); Output.TTW = tangentToWorld; Output.TexCoord = inTexCoord+xTime/50.0f*float2(-1,0); return Output; 

You pass the Tangent-to-World matrix and the texture coordinates used for sampling from the concave-convex ing to the pixel shader.

Pixel Shader: concave-convex ing

In each pixel, you will change the normal direction to fill in small fluctuations in the water surface. If you use a concave-convex texture, you can easily see the pattern on the concave-convex texture in the final result. For each pixel, You need to sample the three color channels of the concave-convex texture and calculate the average of the result. Remember that colors are always defined in the [0, 1] interval, So you subtract 0.5 from each component of the color so that they are in the [-0.5, 0.5] interval. Next, map them to the [-] range (the XYZ coordinate range of the normal ).

float3 bumpColor1 = tex2D(BumpMapSampler, xTexStretch*PSIn.TexCoord)-0.5f; float3 bumpColor2 = tex2D(BumpMapSampler, xTexStretch*1.8*PSIn.TexCoord.yx)-0.5f; float3 bumpColor3 = tex2D(BumpMapSampler, xTexStretch*3.1*PSIn.TexCoord)-0.5f; float3 normalT = bumpColor1 + bumpColor2 + bumpColor3; 

These three texture coordinates are different because they are multiplied by different factors. The XY coordinate of the second texture has been changed. Finally, the three offsets are added.

This direction must be normalized, but before that, you can also scale the concave and convex textures. In the concave-convex texture, the blue vector corresponds to the default normal vector, and the red and green correspond to the offset of the subnormal and tangent. Therefore, if you increase/decrease the red or green color, you can increase/decrease the offset, and thus changed the concave-convex ing!

normalT.rg *= xBumpStrength; normalT = normalize(normalT); float3 normalW = mul(normalT, PSIn.TTW); 

The direction obtained in the code above needs to be normalized and converted to the world space. You finally get the normal in the world space, which can be computed with vectors in other world spaces.

Pixel Shader: Reflection

With the normal vector defined in the world space, we can now calculate the illumination of the water surface. But in order to make the effect more realistic, you need to first reflect the water surface. Because the color of the reflection is sampled from the sky box, you need to sample the direction from the sky box (see tutorial 2-8 ). You can obtain the reflection direction from the image of the eye vector about the normal, as shown in Figure 5-34.

Figure 5-34 obtain the reflection vector from the image using the eye vector's normal

You can subtract the initial vertex from the target point to obtain the eye vector:

float3 eyeVector = normalize(PSIn.Pos3D - xCameraPos); 

Because the eye vector box normal vectors are all defined between them, you can operate on them. HLSL provides the reflect Method to Calculate the reflection vectors of 5-34:

float3 reflection = reflect(eyeVector, normalW); 

If you sample the texture of the sky box from this direction, you can get the reflection color of the current pixel (visible tutorial 2-8 get more about texCUBE ):

float4 reflectiveColor = texCUBE(CubeMapSampler, reflection); 
Pixel Shader: fresh item

If you simply apply a reflection color, the reflection will be the same in any place on the water surface, which will make the water surface look like a tumble mirror. To solve this problem, you need to adjust the reflectivity according to the observation angle. If you observe in parallel to the water surface, the reflectivity will be very high. If you observe vertically, the water surface will be dark blue if the reflectivity is low.

Note: On page 276th of the new concept physics-optics tutorial. c. escher's Three Worlds image shows that the light on the interface between water and air comes from a distance with a large angle of incidence, primarily reflection, only the reflection of the tree can be seen. It comes from the close of a small angle of incidence, mainly transmission, that is, the reflectivity is very small, and the underwater fish can be clearly seen.

This can be represented by the point multiplication of the normal vector of the eye vector box. If you observe the water surface vertically, the angle between the two vectors is 0, and the point product is the largest (if both vectors are units vectors, the angle is 1 ). If you observe in parallel to the water surface, the angle between the two vectors is 90 degrees, and the point product is 0. The point multiplication of the Eye vector box's normal vector is called the Fresnel term ).

A large fresh term (close to 1) represents the smallest reflection, while a pixel with a small fresh term represents its behavior like a mirror. The water surface will never look like a mirror, so you need to scale the fresh item from [0.5] to [, 1] to reduce the reflectivity:

float fresnelTerm = dot(-eyeVector, normalW); fresnelTerm = fresnelTerm/2.0f+0.5f; 

Note:The negative signs in the first line of the Code are required because the two vectors are in the opposite direction. The normal points to the top and the eye vector points to the bottom. If no negative signs exist, the fresh item is negative.

The effect is good, but there is no reflection on the sun.

Pixel Shader: mirror reflection

Now you know the color of reflection, the degree of the mixture of reflection and dark blue. You can add some mirror reflection to improve the final effect.

As explained in tutorial 6-4, the mirror reflection usually depends on the direction of the incident light. In this example, we can find the points on the surface of the light source in the reflection cube texture to get better results. They usually correspond to the bright white part of the sky box.

To locate a bright reflection position, add and sum the three color channels of the reflection color. The bright area and area are close to 3. Divide the value by 3 to make it in the [0, 1] range.

float sunlight = reflectiveColor.r; sunlight += reflectiveColor.g; sunlight += reflectiveColor.b; sunlight /= 3.0f; float specular = pow(sunlight,30); 

Calculate the value by the power of 30, so that only the brightest color is left. Only the sunlight value greater than 0.975 will get the specular value exceeding 0.5!

Pixel Shader: Integrated

Finally, you can combine all the elements to obtain the final color. The following is the code:

float4 waterColor = float4(0,0.2,0.4,1); Output.Color = waterColor*fresnelTerm + reflectiveColor*(1-fresnelTerm) + specular; 

The relationship between the dark blue water surface color and the reflected color is determined by the fresh item. For almost all pixels, this combination of colors forms the final color. For a pixel with a very bright reflective color, the specular value will be greater than 1, which will make the final color brighter. The very bright reflection of the surface Location corresponds to the sun position in the sky box.

Final Effect

Code XNA Section

In the XNA project, you can fully control the wave. You can set the velocity, amplitude, and wavelength of four waves independently. To remove a wave, set its amplitude to 0. The following code sets all effect parameters and draws a sea surface triangle:

Vector4 waveSpeeds = new Vector4(1, 2, 0.5f, 1.5f); Vector4 waveHeights = new Vector4(0.3f, 0.4f, 0.2f, 0.3f); Vector4 waveLengths = new Vector4(10, 5, 15, 7); Vector2[] waveDirs = new Vector2[4]; waveDirs[0] = new Vector2(-1, 0); waveDirs[1] = new Vector2(-1, 0.5f); waveDirs[2] = new Vector2(-1, 0.7f); waveDirs[3] = new Vector2(-1, -0.5f); for (int i = 0; i < 4; i++)     waveDirs[i].Normalize(); effect.CurrentTechnique = effect.Techniques["OceanWater"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xBumpMap"].SetValue(waterBumps); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xBumpStrength"].SetValue(0.5f); effect.Parameters["xCubeMap"].SetValue(skyboxTexture); effect.Parameters["xTexStretch"].SetValue(4.0f); effect.Parameters["xCameraPos"].SetValue(fpsCam.Position); effect.Parameters["xTime"].SetValue(time); effect.Parameters["xWaveSpeeds"].SetValue(waveFreqs); effect.Parameters["xWaveHeights"].SetValue(waveHeights); effect.Parameters["xWaveLengths"].SetValue(waveLengths); effect.Parameters["xWaveDir0"].SetValue(waveDirs[0]); effect.Parameters["xWaveDir1"].SetValue(waveDirs[1]); effect.Parameters["xWaveDir2"].SetValue(waveDirs[2]); effect.Parameters["xWaveDir3"].SetValue(waveDirs[3]); effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) {    pass.Begin();     device.Vertices[0].SetSource(waterVertexBuffer, 0, VertexPositionTexture.SizeInBytes);     device.Indices = waterIndexBuffer;     device.VertexDeclaration = myVertexDeclaration;     device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, waterWidth *waterHeight,     0, waterWidth * 2 * (waterHeight - 1) - 2);     pass.End(); } effect.End(); 

Note:Wave parameters must be set only when the wave changes. Other parameters, such as time, view matrix, and camera position, must be updated at each frame or when the camera position changes.

HLSL

You can find the XNA-to-HLSL variable, texture, and output structure at the beginning of this tutorial.

The following is the vertex shader code. vertex shader constantly changes the height of each vertex and computes the Tangent-to-World matrix:

OWVertexToPixel OWVertexShader(float4 inPos: POSITION0, float2 inTexCoord:TEXCOORD0){    OWVertexToPixel Output = (OWVertexToPixel)0;         float4 dotProducts;     dotProducts.x = dot(xWaveDir0, inPos.xz);     dotProducts.y = dot(xWaveDir1, inPos.xz);     dotProducts.z = dot(xWaveDir2, inPos.xz);     dotProducts.w = dot(xWaveDir3, inPos.xz);     float4 arguments = dotProducts/xWaveLengths+xTime*xWaveSpeeds;     float4 heights = xWaveHeights*sin(arguments);     float4 final3DPos = inPos;     final3DPos.y += heights.x;     final3DPos.y += heights.y;     final3DPos.y += heights.z;     final3DPos.y += heights.w;         float4x4 preViewProjection = mul(xView, xProjection);     float4x4 preWorldViewProjection = mul(xWorld, preViewProjection);     Output.Position = mul(final3DPos, preWorldViewProjection);     float4 final3DPosW = mul(final3DPos, xWorld);     Output.Pos3D = final3DPosW;         float4 derivatives = xWaveHeights*cos(arguments)/xWaveLengths;     float2 deviations = 0;     deviations += derivatives.x*xWaveDir0;     deviations += derivatives.y*xWaveDir1;     deviations += derivatives.z*xWaveDir2;     deviations += derivatives.w*xWaveDir3;         float3 Normal = float3(-deviations.x, 1, -deviations.y);     float3 Binormal = float3(1, deviations.x, 0);     float3 Tangent = float3(0, deviations.y, 1);     float3x3 tangentToObject; tangentToObject[0] = normalize(Binormal);     tangentToObject[1] = normalize(Tangent);     tangentToObject[2] = normalize(Normal);     float3x3 tangentToWorld = mul(tangentToObject, xWorld);     Output.TTW = tangentToWorld;     Output.TexCoord = inTexCoord+xTime/50.0f*float2(-1,0);     return Output; } 

Pixel shader blends the dark blue water surface color with the reflection color. The degree of mixing depends on the angle of view, represented by the fresh term. The reflective item adds a highlight to the position on the water surface of the light source in the corresponding environment.

OWPixelToFrame OWPixelShader(OWVertexToPixel PSIn) : COLOR0 {    OWPixelToFrame Output = (OWPixelToFrame)0;         float3 bumpColor1 = tex2D(BumpMapSampler, xTexStretch*PSIn.TexCoord)-0.5f;     float3 bumpColor2 = tex2D(BumpMapSampler, xTexStretch*1.8*PSIn.TexCoord.yx)-0.5f;     float3 bumpColor3 = tex2D(BumpMapSampler, xTexStretch*3.1*PSIn.TexCoord)-0.5f;     float3 normalT = bumpColor1 + bumpColor2 + bumpColor3;     normalT.rg *= xBumpStrength;     normalT = normalize(normalT);     float3 normalW = mul(normalT, PSIn.TTW);     float3 eyeVector = normalize(PSIn.Pos3D - xCameraPos);    float3 reflection = reflect(eyeVector, normalW);     float4 reflectiveColor = texCUBE(CubeMapSampler, reflection);     float fresnelTerm = dot(-eyeVector, normalW);     fresnelTerm = fresnelTerm/2.0f+0.5f;         float sunlight = reflectiveColor.r;     sunlight += reflectiveColor.g;     sunlight += reflectiveColor.b; sunlight /= 3.0f;     float specular = pow(sunlight,30);     float4 waterColor = float4(0,0.2,0.4,1);     Output.Color = waterColor*fresnelTerm + reflectiveColor*(1-fresnelTerm) + specular;         return Output; } 

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.