The new standard WebGL 2 has recently become available in the latest versions of Firefox and Chrome, so there was a desire to try out some new features. One of the most useful and sought-after features of WebGL 2 (and OpenGL ES 3.0, on which it is based) is geometry duplication (eng. Instanced rendering). This feature allows you to reduce the number of draw calls by repeatedly drawing the same geometry with changed parameters. This feature was present in some implementations of WebGL 1, but required the presence of a specific extension. Most often, this function is used to create systems of particles and vegetation, but also quite often it is used to simulate fur.
Concept and demo
There are quite a few different approaches in the simulation of fur in OpenGL, but this implementation is based on the technique described in this video tutorial . Despite the fact that it described the creation of a shader for Unity, detailed and visual step-by-step instructions from this video were taken as the basis for creating an OpenGL ES shader from scratch. If you are not familiar with the general principles of simulation of fur, we recommend you spend 13 minutes watching this video to understand the general principles of its operation.
All graphic materials for the demo were created from scratch (just looking at the photos of different samples of wool). These textures are very simple and their creation did not require any special skills in creating realistic textures. ')
View the finished demo here . If the browser does not support WebGL 2 (for example, at the moment mobile browsers only support WebGL 1), then here are the video demos:
Implementation
To visually show how the fur simulation works, let's start by rendering two additional fur layers with a sufficiently large thickness (the distance between the layers). The following image shows the original object without fur and two translucent layers on top of it:
Increasing the number of layers and reducing the thickness of the layer, we gradually get a more realistic result. In this image of 6 relatively dense layers, a gradual decrease in the transparency of the layers from fully opaque to fully transparent is already clearly visible:
And quite a realistic end result using 20 very thin layers:
Our demo uses 5 different preset parameters - 4 furs and one moss. All of them are rendered by the same shaders but with different specified parameters.
First, a cube is rendered with the same diffuse texture that is used for all layers of fur. However, it must be darkened in order for it to merge with the first layer of fur, so the color from the texture is multiplied by the initial color of the fur. It uses the simplest shader that simply takes the color from the texture and multiplies it by another color.
Next begins drawing layers of fur. They are transparent and therefore require the selection of the correct color mixing mode in order to look realistic. Using the usual glBlendFunc () resulted in either too bright, or too dim fur colors, since this mode affects the alpha channel and thus distorts colors. The glBlendFuncSeparate () function allows you to set different color blending modes for RGB and alpha channels of fragments and using it you managed to keep the alpha channel unchanged (it is fully controlled by the shader) and at the same time correctly mix the color of each fur layer with itself and another geometry .
After the correct color mixing mode is set, you can proceed to the actual drawing of the fur. Drawing all the fur is implemented in one call - all the work on duplicating geometry is performed in one shader. A video card without driver participation repeats geometry drawing a specified number of times and, therefore, there are no costs for additional calls to OpenGL commands. All subsequent clarifications apply only to this shader. The syntax for GLSL 3.0 used in WebGL 2 and OpenGL ES 3.0 is slightly different from GLSL 1.0 — see the differences and instructions for porting old shaders here .
To create layers of fur, the shader shifts each vertex in the direction of the normal. This gives some flexibility in setting the direction of laying wool, because the normals of the model can be tilted if desired (in the demo the normals are perpendicular to the main geometry). From the built-in variable gl_InstanceID, the shader gets the value of the current geometry instance. The greater this value, the further the vertex shifts:
float f = float(gl_InstanceID + 1) * layerThickness; // calculate final layer offset distance vec4 vertex = rm_Vertex + vec4(rm_Normal, 0.0) * vec4(f, f, f, 0.0); // move vertex in direction of normal
In order for the fur to look realistic, it must be more dense at the base and gradually taper at the tips. This effect is achieved by gradually changing the transparency of the layers. Also, to simulate ambient occlusion, the fur should be darker at the base and lighter on the surface. Typical parameters for fur are the initial color [0.0, 0.0, 0.0, 1.0] and the final color [1.0, 1.0, 1.0, 0.0]. Thus, the fur begins completely black and ends with the color of the diffuse texture, while the transparency increases from a completely opaque layer to a fully transparent one.
First, in the vertex shader, the color coefficient is calculated and, based on this coefficient, the color between the initial and final color is interpolated. In a fragmentary shader, this color is multiplied by the color of the diffuse texture. The final stage is the multiplication of the alpha channel of the fragment by the color from the black and white texture, which determines the distribution of wool.
// vertex shader float layerCoeff = float(gl_InstanceID) / layersCount; vAO = mix(colorStart, colorEnd, layerCoeff); // fragment shader vec4 diffuseColor = texture(diffuseMap, vTexCoord0); // get diffuse color float alphaColor = texture(alphaMap, vTexCoord0).r; // get alpha from alpha map fragColor = diffuseColor * vAO; // simulate AO fragColor.a *= alphaColor; // apply alpha mask
There are many possible options for the implementation of waving fur in the wind. In our demo, we slightly displace each vertex based on the cyclically variable parameter passed to the shader. In order for all layers to move synchronously, it is necessary to calculate a certain unique value for each vertex with the same coordinates. In this case, it will not be possible to use the built-in variable gl_VertexID , since its value differs for different vertices, even for those whose coordinates are the same. So we compute a certain “magic sum” from the coordinates of the vertex and use it in a sinusoidal function to create the “waves” of the wind. An example of vertex offset based on the value of the time parameter:
constfloat PI2 = 6.2831852; // Pi * 2 for sine wave calculation const float RANDOM_COEFF_1 = 0.1376; // just some random float float timePi2 = time * PI2; vertex.x += sin(timePi2 + ((rm_Vertex.x+rm_Vertex.y+rm_Vertex.z) * RANDOM_COEFF_1)) * waveScaleFinal; vertex.y += cos(timePi2 + ((rm_Vertex.x-rm_Vertex.y+rm_Vertex.z) * RANDOM_COEFF_2)) * waveScaleFinal; vertex.z += sin(timePi2 + ((rm_Vertex.x+rm_Vertex.y-rm_Vertex.z) * RANDOM_COEFF_3)) * waveScaleFinal;
Further improvements
Despite a fairly realistic result, this implementation of the fur can be significantly improved. For example, you can implement the use of force and wind direction and have wool of different lengths in different areas of the model, setting the length coefficients for each vertex.
You can take the code from Githab , use it and improve it in your projects - it uses the MIT license.