Translation Three.js is a great open source WebGL library. WebGL allows JavaScript to operate the GPU and achieve true 3D on the browser side. However, this technology is still in the development stage, and the information is extremely scarce. Enthusiasts basically have to learn through the Demo source code and the source code of Three.js itself.
. Introduction This is the second half of the WebGL shader tutorial. If you have not read the previous one, reading this tutorial may make you confused. It is recommended You read the previous tutorial.
At the end of the previous article, we drew a beautiful pink sphere in the center of the screen. Now I'm going to start creating something more interesting.
In this tutorial, we will take a moment to add an animation loop, then vertex attributes variables and a uniform variable. We also need to add some varying variables so that the vertex shader can pass information to the fragment shader. The end result is a pink sphere that "ignites" from the top to the sides and then moves in a regular pattern. This is a bit psychedelic, but it will help you get a better understanding of the three variables in the shader: they are related to each other and implement the entire assembly. Of course we will do this in the Three.js framework.
1. Simulate lighting Let’s update the colors so the sphere doesn’t look like a flat, dark circle. If we want to see how Three.js handles lighting, I'm sure you'll find it's a lot more complicated than we need, so let's simulate lighting first. You should take a look at the fantastic shaders in Three.js, as well as some from a recent WebGL project by Chris Milk and Google, Rome.
Back to the shader, we need to update the vertex shader to pass the vertex normal vector to the fragment shader. Use a varying variable:
// Create a varying variable vNormal , both the vertex shader and the fragment shader contain this variable
varying vec3 vNormal;
void main() {
// Set vNormal to normal, which is created by Three.js and passed to The attribute variable of the shader
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
}
in the fragment In the shader, we will create a variable with the same name, then dot-multiply the normal vector with another vector representing the ray from the top right, and apply the result to the color. The end result looks a bit like directional light.
// The same variable vNormal as in the vertex shader
varying vec3 vNormal;
void main() {
// Define the light vector
vec3 light = vec3(0.5,0.2,1.0);
// Make sure it is normalized
light = normalize(light);
// Calculate the dot product of the light vector and the normal vector. If the dot product is less than 0 (that is, the light cannot reach it), set it to 0
float dProd = max(0.0, dot(vNormal, light));
// Fill fragment color
gl_FragColor = vec4(dProd, // R
dProd, // G
dProd, // B
1.0) ; // A
}
The reason for using dot product is: the dot product of two vectors shows how "similar" they are. If both vectors are normalized and their directions are exactly the same, the value of the dot product is 1; if the directions of the two vectors happen to be exactly opposite, the value of the dot product is -1. What we do is apply the dot product value to the fiber, so if this point is on the upper right side of the sphere, the dot product value is 1, which means it is fully illuminated; and for the point on the other side, we get The dot product value is close to 0 or even -1. Any negative values we get are set to 0. When you pass in the data, you will see the most basic lighting effect.
What’s below? We will incorporate the coordinates of the vertices.
2. Attribut variable Next I need to pass a random number to each vertex through the Attribute variable. This random number is used to push the vertex a certain distance along the normal vector. The new result is a bit like a weird irregular object that changes randomly every time the page is refreshed. For now, he won't move (I'll make him move later), but after a few refreshes you can nicely see that his shape is random.
Let’s start adding attribute variables to the vertex shader:
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// Convert the random number displacement into a three-dimensional vector, so that it can be combined with The normals are multiplied
vec3 newPosition = position
normal * vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition, 1.0);
}
You see nothing has changed, because the attribute variable displacement has not been set yet, so the shader uses 0 as the default value. Displacement has not yet taken effect at this time, but we will soon add attribute variables to the shader material, and then Three.js will automatically tie them together and run them.
Also note the fact that I assigned the updated position to a new three-dimensional vector variable, because the original position variable, like all attribute variables, is read-only .
3. Update the shader material Now we update the shader material and pass something into the attribute object displacement. Remember, attribute objects correspond to vertices one-to-one, so we have a value for each vertex of the sphere, like this:
var attributes = {
displacement: {
type: 'f', // Floating point number
value: [] // Empty array
}
};
var vShader = $('#vertexshader');
var fShader = $('#fragmentshader');
// Create an attribute containing Shader Material
var shaderMaterial =
new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: vShader.text(),
fragmentShader: fShader.text()
} );
// Fill displacement with random numbers
var verts = sphere.geometry.vertices;
var values = attributes.displacement.value;
for(var v = 0; v < verts.length; v ) {
values.push(Math.random() * 30);
}
In this way, you can see a deformed sphere. The coolest thing is: all these transformations are done in the GPU.
4. Move it What should I do to make this thing move? Well, should do these two things.
A uniform variable amplitude controls the actual displacement caused by displacement at each frame. We can use the sine or cosine function to generate it in each frame, as these two functions take values from -1 to 1.
A frame loop.
We need to add this uniform variable to the shader material, and also need to add it to the vertex shader. Let’s look at the vertex shader first:
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// Multiply displacement by amplitude as we smoothly change amplitude in each frame , the screen moves
vec3 newPosition =
position
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4 (newPosition, 1.0);
}
Then update the shader material:
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
} ;
var vShader = $('#vertexshader');
var fShader = $('#fragmentshader');
// Create the final shader material
var shaderMaterial =
new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: vShader.text(),
fragmentShader: fShader.text()
});
Our shader is also ready. But we seem to have taken another step backward, and there are only smooth balls left on the screen. Don't worry, this is because the amplitude value is set to 0, and since we multiplied the amplitude by the displacement, we won't see any change now. We haven't set up the loop yet, so amplitude can only be 0.
In our JavaScript, we need to package the rendering process into a function, and then use requestAnimationFrame to call the function. In this function, we update the value of uniform (Translator's Note: amplitude).
var frame = 0;
function update() {
// amplitude comes from the sine value of frame
uniforms.amplitude.value =
Math.sin(frame);
// Update the global variable frame
frame = 0.1;
renderer.render(scene, camera);
// Specify that the next time the screen is refreshed, call update
requestAnimFrame(update);
}
requestAnimFrame(update);
5. Summary That’s it! You see the sphere pulsing strangely. There's a lot that I haven't covered yet about shaders, but I hope this tutorial was helpful. Now, when you see some of the other shaders, I hope you understand them, and you should feel confident enough to create your own!
As usual, I packaged the
source code of this lesson