Lesson 15 - Textured Vector Fonts

 

Introduction

Lesson 15 is essentially a sequel to the previous lesson on vector fonts. The NeHe lesson was about how to apply texture to vector fonts. In OpenGL 1.1 that required a bit of work. In three.js it is actually pretty trivial as one can just create a material and specify a texture. However, that seems too easy, so we'll show you another way to do it - by writing your first shader program. The shader program is pretty trivial, but it introduces the concept. We'll encounter some more complex shaders later in the other lessons.

About Shaders

OpenGL Shading Language (GLSL), is a high-level shading language based on the syntax of the C programming language. It was created by the OpenGL Architecture Review Board to give developers more direct control of the graphics pipeline without writing in assembly language. When your JavaScript program is executed by the browser, three.js takes the objects you have created and added to your scene, pushes them into vertex buffers and passes them to the GPU. The default shaders of three.js as well as any shaders you have defined to are compiled and passed to the GPU as well, where they use the data in the vertex buffers to render the actual pixels.

That rendering process is bit complex. A good article on it is here. But the gist is this. You define (via three.js meshes) a set of vertices. For each vertex a call is made to a vertex shader, which figures out where that vertex is mapped onto your display. Then WebGL takes the set of vertices as mapped to the display and iterates across the display and for each pixel calls the fragment shader to determine what color to make the pixel.

You pass information to the shaders via what are called uniforms. Information is passed from the vertex shader to the fragment shader via variables called varying. This all sounds kind of complex, but let's look at an actual example.

We first load the images into textures.

wood = new THREE.ImageUtils.loadTexture("images/wood-grain.jpg");
leather = new THREE.ImageUtils.loadTexture("images/green-pebbled-leather.jpg");

We need to ensure that our texture gets wrapped so the coverage of our meshes is complete and uniform. Also note that the texture needs to be big enough (e.g. at least 512x512) or the sharper curves will be so stretched that the textures will be blurred.

wood.wrapS = wood.wrapT = THREE.RepeatWrapping;
leather.wrapS = leather.wrapT = THREE.RepeatWrapping;

Now, add a uniform for each texture. These are the read-only variables that get passed to our shaders

var uniformsW = {
    woodImage:	{ type: 't', value: wood }
};

var uniformsL = {
    leatherImage:	{ type: 't', value: leather }
};

Then we need to find the shaders, whach are inside special script elements that we'll look at shortly. Note that we only have one vertex shader since what is does is exactly the same for both the face and the sides of the mesh, so we only need one.

var vs = document.getElementById("vertexShader").textContent;
var fsW = document.getElementById("fragmentShader-W").textContent;
var fsL = document.getElementById("fragmentShader-L").textContent;

Next we create the shader material and store it in an array, just as in the previous lesson. The key difference here is that we pass in the uniforms and pointers to the THREE.ShaderMaterial.

var shaderMaterialW = new THREE.ShaderMaterial({
    uniforms:       uniformsW,			
    shading:        THREE.FlatShading,
    side:           THREE.DoubleSide,	
    vertexShader:   vs,				
    fragmentShader: fsW
});

var shaderMaterialL = new THREE.ShaderMaterial({
    uniforms:       uniformsL,			
    shading:        THREE.FlatShading,
    side:           THREE.DoubleSide,	
    vertexShader:   vs,					
    fragmentShader: fsL
});

var materialArray = [ shaderMaterialL, shaderMaterialW ];

The rest of the JavaScript is exactly the same as the previous lesson; we load the font, instantiate the TextGeometry and create the text-mesh, passing it the TextGeometry and our materialArray.

The Shaders

Finally, let's take a look at the shaders themselves. The vertex shader is always called first. Also note that the mimetype of the shader is "x-shader/x-vertex". The browser doesn't know what to do with such a mimetype so it just ignores it. three.js takes care of fetching the script element, injecting some variables and then feeding it to the WebGL API.

<script id="vertexShader" type="x-shader/x-vertex">
    varying vec3   vNormal;
    varying vec2   vUv;		

    void main() {
    
        vNormal = normal;
        vUv = uv;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
</script>

Three.js injects (behind the scenes, prior to the call the vertex shader) the variables normal and uv. normal is the normal vector for the vertex being operated on. uv is exactly what you might guess, it is the UV index (just like in our earlier texture-mapping lessons) into the texture being passed in in our uniform. We assign normal and uv to two variables we declare as varying which makes them available to the fragment shader. In this case, the fragment shader doesn't need the normal, but we do it anyway, just for example's sake. The position variable is vector which holds the coordinates of the actual vertex.

Then we simply transform the vertex's position by multiplying it by the modelViewMatrix and the projectionMatrix (both provided by three.js) to get a final vertex position.

Finally, let's look at the fragment shader. They're virtually identical since they do the same thing. Again, note that the mimetype of the shader is now "x-shader/x-fragment". The browser doesn't know what to do with such a mimetype so it just ignores it.

<script id="fragmentShader-W" type="x-shader/x-fragment">
    varying vec2         vUv;
    uniform sampler2D    woodImage;

    void main(void) {
			
        gl_FragColor = texture2D(woodImage, vUv);
    }
</script>

Note the declaration of the varying vec2 that we declared in the vertex shader. It contains the indices we need to figure out how to index into the texture passed in via our uniform. We then determine the gl_fragColor by calling a function, texture2D, which is a wrapper for the underlying low-level WebGL API. The other fragment shader is identical except that it's uniform sampler2D is leatherImage.

And that's it! Click on this link to see the actual rendered demo in all its texture filtered glory! Shazaam!

As always, the original sources are on github here.



About Us | Contact Us | ©2017 Geo-F/X