Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για...

27
University of Crete Department of Computer Science Δημήτρης Καμηλέρης ([email protected]) Ηράκλειο 22/8/2012 Επιβλέπων καθηγητής: Γεώργιος Παπαγιαννάκης Dimitris Kamileris Heraklion 22/8/2012 Instructor: George Papagiannakis Diploma thesis: Real-time shader-based Per-fragment lighting for dynamic objects in WebGL

Transcript of Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για...

Page 1: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

University of Crete

Department of Computer Science

Δημήτρης Καμηλέρης ([email protected])

Ηράκλειο 22/8/2012

Επιβλέπων καθηγητής: Γεώργιος Παπαγιαννάκης

Dimitris Kamileris

Heraklion 22/8/2012

Instructor: George Papagiannakis

Diploma thesis:

Real-time shader-based Per-fragment lighting for dynamic objects in WebGL

Page 2: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

2

Περίληψη

H WebGL είναι μια νέα γλώσσα προγαμματισμού που ειδικεύεται στην ανάπτυξη τρισδιάστατων

εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την εργασία εξετάζονται

τα βασικά σημεία αυτής της γλώσσας και αναλύονται κάποιοι από τους μηχανισμούς λειτουργίας

της. Αυτό γίνεται υλοποιόντας το ανα-εικονοτεμαχίδιο (per-fragment) μοντέλο φωτισμού Phong.

Abstract

WebGL is a new programming language oriented in developing 3D applications for modern web

browsers. This thesis goes over the basic points of WebGL and explains some of the

mechanisms. This is done by implementing the per-fragment Phong reflection model on a

dynamic 3D object.

Page 3: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

3

Table of Contents

Introduction ...................................................................................................................... 4

WebGL compatible browsers ............................................................................................... 5

WebGL application structure ............................................................................................... 5

Development of a WebGL application ................................................................................... 5

The WebGLRenderingContext.............................................................................................. 5

The WebGL rendering pipeline ............................................................................................ 6

The WebGL buffers ............................................................................................................ 8

Constructing the buffers ............................................................................................... 8

Rendering the buffers .................................................................................................. 9

Shaders and Lighting .........................................................................................................11

Initializing the shaders ................................................................................................11

Attributes and Uniforms ..............................................................................................12

Attributes ...............................................................................................................12

Uniforms ................................................................................................................13

Normals .................................................................................................................13

Lighting: the Phong reflection model ............................................................................14

Per-Fragment Phong implementation ...........................................................................15

The viewing transformations ..............................................................................................17

Movement ........................................................................................................................19

Textures ...........................................................................................................................20

Loading a Collada 3D object ...............................................................................................23

Conclusion - Future plans ..................................................................................................26

Page 4: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

4

Introduction

The need for high performance that dominates every branch of computer science could

not have let uninfluenced webpage developers who need fast hardware accelerated interactive

3D graphics. So far the only way of rendering graphics on the web was by loading separate

applications as plug-ins. Plug-ins are often problematic and need a user-side installation.

Applications what are executed over plug-ins are usually developed in high level languages

which tend to have worse performance than low level languages. In order to cover the needs of

3D web applications, Khronos™ group has developed WebGL™.

WebGL is a new technology integrated into web standards (e.g. HTML, DOM) and allows

rendering hardware accelerated interactive 2D and 3D graphics within any major modern web

browser without the use of plug-ins. To be more specific WebGL is a low-level JavaScript API

based on OpenGL ES 2.0 exposed through the HTML 5 canvas element. The HTML

CanvasElement is a new feature adopted since HTML 5 and allows for dynamic rendering of 2D

or 3D scenes via scripting languages (e.g. JavaScript). Like the OpenGL ES 2.0, WebGL is

Shader-based using the GLSL shading language. –Shaders are small programs that are

executed by the Graphics Processing Unit (GPU) –. WebGL is cross-platform and royalty-free the

only restriction of using it is that the user must have a compatible web browser. Luckily most

major web browsers Safari, Chrome, Firefox and Opera support WebGL on their newest

distributions.

Another issue of modern 3D application development is the vast majority of different 3D

object file formats. Each modeling software (Maya, 3D Studio Max, Blender etc) uses its own

file format and this creates compatibility problems and makes sharing and transporting difficult

among 3D applications. To solve this problem the Collada file format has been introduced.

Collada introduces a new XML (Extensible Markup Language) schema to make it easy to

transport 3D assets between applications without loss of information or compatibility problems.

In this thesis I explain how WebGL works in detail and how it renders 3D objects. I also

explain how a Collada 3D object can be loaded into a WebGL application and how it can be

illuminated using GLSL shaders.

Page 5: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

5

WebGL compatible browsers

As mentioned earlier WebGL is based completely plug-in free so the only restriction in

order to run and start developing WebGL applications is to have a compatible browser. Mozilla

Firefox supports WebGL since version 4.0, Google Chrome since version 9, Opera 11 and 12

also support WebGL. Safari supports WebGL since version 5.1 but it is disabled by default so it

needs to be enabled in order to view WebGL content. Finally Microsoft Internet Explorer does

not support WebGL and so far Microsoft has not shown any intention of supporting it in the

future.

WebGL application structure

A typical WebGL application consists of the following components:

Canvas: the canvas consists of a drawable region defined in HTML code with height

and width attributes and is accessible through JavaScript code. Canvas is the

placeholder where the scene will be rendered.

Objects: WebGL buffers contain all the data necessary in order to render the 3D

objects that make up the scene.

Lighting: Lighting makes the 3D scene visible. WebGL uses GLSL shaders in order to

illuminate the scene created by the buffers and to add a more realistic tone.

Development of a WebGL application

The WebGLRenderingContext

A WebGL context is a JavaScript object through which we can access all the WebGL functions and attributes this means WebGL's Application Program Interface (API). As mentioned in the introduction WebGL is exposed through the canvas element so in order to

develop a WebGL application the first step is to create a canvas.

<canvas id="Canvas1" width="640" height="480"></canvas>

This line of code is the only HTML code really necessary inside the HTML body. The rest of the

application is written in JavaScript. After creating the canvas it is time to initialize the WebGL

context. In order to do it the canvas created in the HTML body is needed. The way to get the

canvas into the script is to use JavaScript’s method getElementById. This method will find

Page 6: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

6

any HTML element with the specified id on the current page or as it is referred in the HTML

“document” and return it.

var canvas = document.getElementById("Canvas1");

The WebGL context now can be created by the canvas

var gl = canvas.getContext("experimental-webgl");

The context name "experimental-webgl" is temporary and will be replaced by "webgl"

soon in the future.

At this point a WebGLRenderingContext object has been created and the WebGL API can be

accessed by the variable gl. For example the function clearColor that WebGL uses to

clear the canvas is called by this manner: gl.clearColor(0.0, 0.0, 0.0, 1.0); The

four float values passed as parameters are the RGBa values of the desirable color.

The WebGL rendering pipeline

Here follows a simplified diagram of WegGL’s rendering pipeline and a simple description of

each segment.

Buffers Attributes

Vertex Shader

Uniform Variables

JavaScript

Fragment Shader

Varying Variables

Frame Buffer

Page 7: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

7

1. Buffers: The buffers contain the data that WebGL needs in order to render anything

from a simple 2D shape to a full 3D complicated scene. There are several data objects

that buffers can contain such as vertex coordinates, vertex normals and texture

coordinates.

2. Vertex Shader: The vertex shader operates on each vertex and handles per-vertex data

such as vertex and texture coordinates colors and vertex normals. This data is

represented by attributes inside the vertex shader. Each attribute points to the buffer

from where it reads the data.

3. Fragment Shader: Every three vertices form up a triangle, the surface of this triangle

consists of elements called fragments fragment shader’s job to calculate the color of

each fragment.

4. Attributes: are input variables used in the vertex shader. Because the vertex shader is

called upon each vertex, the attributes will be different every time the shader is called.

5. Uniforms: are input variables for the vertex and also the fragment shader. Uniforms do

not change throughout the rendering cycle.

6. Varyings: are used to pass data from the vertex to the fragment shader.

7. Framebuffer: The framebuffer is the buffer that contains the fragments already

processed by the fragment shader. Once the fragment shader has completed its job the

framebuffer is imprinted on the screen.

Page 8: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

8

The WebGL buffers

Constructing the buffers

WebGL buffers are constructed from JavaScript arrays because the API does not provide

methods to pass independent vertices to the rendering pipeline like OpenGL does. In order to

create 3D objects there are used two kinds of buffers the vertex and the index buffers. The

vertex buffer that contains the coordinates of all the vertices those make up the scene, and the

index buffer. The index buffer contains numbers that correspond in vertices in the vertex buffer.

Indices show how the vertices connect in order to form up triangles.

For example the vertex array for the square in Figure 1 is

x y z x y z x y z x y z

[0,0,0, 1.0,0,0, 0,1.0,0, 1.0,1.0,0]

vertex0 vertex1 vertex2 vertex3

and the index array is

[0,1,2,1,2,3]

The index array indicates that the first surface is created by the vertices 0, 1 and 2 and the

other by the vertices 1, 2 and 3.

Once the vertex and the index arrays are created, the respective buffers can be created

as well.

var vertex = [0,0,0, 1.0,0,0, 0,1.0,0, 1.0,1.0,0]; //the arrays as

var index = [0,1,2,1,2,3]; //created previously

var mesh = gl.createBuffer(); //the variable gl holds the WebGL context

The variable mesh is initialized as a WebGL buffer. Because of the fact that buffers are part of

the graphics card memory and not the main memory (RAM), accessing them is not as simple.

In order to perform any operation at a given buffer it must be set as the current buffer. This

means that after setting a buffer as the current buffer any buffer-related operation will be

executed on this buffer.

gl.bindBuffer(gl.ARRAY_BUFFER, mesh);

The first parameter is the type of buffer. There are two types of buffers using the one or the

other depends on the type of data this buffer will hold. The gl.ARRAY_BUFFER holds vertex

data (i.e. vertex coordinates, vertex colors, texture coordinates, and vertex normals). The

gl.ELEMENT_ARRAY_BUFFER is used to hold index data.

Since the buffer mesh is the current buffer the contents can be passed.

Figure 1

Page 9: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

9

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW);

vertex is the JavaScript array created previously holding the vertex coordinates. The

Float32Array is a WebGL data type called typed array and it is used instead of the

JavaScript array because it is in binary form. The WebGL typed arrays are Int8Array, Uint8Array, Int16Array,Uint16Array, Int32Array, UInt32Array,

Float32Array, and Float64Array.

At this point the JavaScript array containing the vertex coordinates is passed in to the respective buffer and it is time to do the same for the index array. var indexbuf = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexbuf);

gl.bufferData(gl. ELEMENT_ARRAY_BUFFER, new Uint16Array (index),

gl.STATIC_DRAW);

Rendering the buffers

In principle the buffers can now be rendered on the screen. Of course without the shaders nothing would be visible but since the shaders will be analyzed a bit further down and the rendering method remains the same there is no harm in showing how the buffers are rendered here.

There are two functions that can render the buffers, the drawArrays and the

drawElements. The first uses the ARRAY_BUFFER in order to render the meshes, the

second uses the ELEMENT_ARRAY_BUFFER in order to access the vertex buffer and then

render on the screen. The drawArrays function is used when the mesh is simple enough for

example a triangle or a square this means that for the more complicated meshed the

drawElements function is used and there is a good reason for it. drawArrays renders

every vertex in the order found in the buffer this means that in order to draw more complicated objects the same vertices need to be more than once in the buffers. The first and obvious problem is the increased buffer size. The second problem is that because there are more vertices the vertex shader will be called more times spoiling the performance. On the other hand when using indices each vertex needs to be in the buffers only once and the vertex shader is called only once upon each vertex. By using the element array buffer for rendering there is a significant improvement of the performance and a great reduction of the required memory. gl.drawArrays(Mode, First, Count);

Mode: The mode specifies what kind of primitives are constructed. Accepted values

are: gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN.

First: specifies the first to be rendered element.

Count: specifies the number of elements to be rendered.

gl.drawElements(Mode, Count, Type, Offset);

Page 10: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

10

Mode: The mode specifies what kind of primitives are constructed. Acceptable

values are: gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN.

Count: specifies the number of elements to be rendered.

Type: indicates the data type of the values in the index buffer. Acceptable values

are: UNSIGNED_BYTE UNSIGNED_SHORT

Offset: Specifies the first to be rendered element

As mentioned earlier no operation can be executed to a buffer unless it is set as the current

buffer. Rendering it is no exception so before calling either the gl.drawArrays or the

gl.drawElements, the gl.bindBuffer must be called.

Page 11: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

11

Shaders and Lighting

Initializing the shaders

The shaders are small programs that are executed by the graphics card and are usually used to model effects such as lights reflections and shades. The shaders are independent from the WebGL application they are even written in a different programming language. The GLSL (OpenGL Shading Language) is used for this purpose. In order for the shaders to be used they need to be compiled and linked to the WebGL application. This process is executed by the WebGL application itself. To be more precise shaders are loaded as string, compiled and attached to a program (the fragment and vertex shaders together are called a program) and then linked to the WebGL application and finally the program is used by the WebGL application. var shaderScript = document.getElementById(shaderid);

var str = "";

var k = shaderScript.firstChild;

while (k) {

if (k.nodeType == 3) {

str += k.textContent;

}

k = k.nextSibling;

}

The piece of code above loads a shader as a string using the DOM tree. Once the var str

contains the loaded source code it’s time to create a shader containing that source code.

WebGL provides the createShader function which creates either a fragment or a vertex

shader. var shader;

if (shaderScript.type == "x-shader/x-fragment") {

shader = gl.createShader(gl.FRAGMENT_SHADER);

} else if (shaderScript.type == "x-shader/x-vertex") {

shader = gl.createShader(gl.VERTEX_SHADER);

} else {

return null;

}

gl.shaderSource(shader, str);

And now compile the shader gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {

alert(gl.getShaderInfoLog(shader));

return null;

}

Page 12: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

12

This process must be performed for both the vertex and the fragment shader. After the compilation has been successfully completed the shaders need to be attached to a program and then the program needs to be linked shaderProgram = gl.createProgram();

gl.attachShader(shaderProgram, vertexShader);

gl.attachShader(shaderProgram, fragmentShader);

gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {

alert("Could not initialise shaders");

}

Finally use the program gl.useProgram(shaderProgram);

At this point the GLSL shaders are part of the rendering pipeline. Attributes and Uniforms

As mentioned earlier attributes and uniforms are input variables to the vertex and

fragment shaders. Input can be any information related to the shaders’ job for example the vertex coordinates for the vertex shader or coloring data for the fragment shader.

Attributes

The buffers can feed their data to the vertex shader by being associated with an

attribute. Each buffer is associated with one and only one attribute. In order to point an attribute to a buffer first of all the buffer must become the current buffer then WebGL’s

function vertexAttribPointer points the attribute to the buffer.

gl.vertexAttribPointer(Index,Size,Type,Norm,Stride,Offset);

Index: refers to the attribute that is going to be associated with the current buffer.

Size: refers to the size of the current buffer.

Type: refers to the data type of the values stored in the current buffer. Acceptable values

are: FIXED, BYTE, UNSIGNED_BYTE, FLOAT, SHORT, or UNSIGNED_SHORT.

Norm: (boolean)Specifies whether fixed-point data values should be normalized or

converted directly as fixed-point values when they are accessed.

Stride: specifies the byte offset between consecutive generic vertex attributes. If

stride is 0, the generic vertex attributes are understood to be tightly packed in the array. The initial value is 0.

Offset: specifies the position in the buffer from which we will start reading

values for the corresponding attribute.

Page 13: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

13

The most common attributes and the ones used in the context of this thesis are: The

vertexPositionAttribute: “aVertexPosition” bound to the buffer that contains the

vertex coordinates, the vertexNormalAttribute: “aVertexNormal” bound to the

normals buffer (will be explained shortly) and finally the textureCoordAttribute:

“aTextureCoord” bound to the buffer that contains the texture coordinates.

Of course before associating the buffers with the attributes they need to be enabled. shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram,

"aVertexPosition");

gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

Uniforms

Uniform variables follow the same principles with the attributes but because they remain

constant throughout the rendering cycle (consecutive calls of drawElements) their values

need to be set whenever they change and not in every cycle. Initialization is done like this:

shaderProgram.ambientColorUniform = gl.getUniformLocation(shaderProgram,

"uAmbientColor");

And pass values like this: gl.uniform3f(shaderProgram.ambientColorUniform,0.2,0.2,0.2);

Normals

A normal is a vector perpendicular to a given surface. A normal vector is used to

calculate the effect of a light source to a given surface because the vector defines an orientation for the surface. In simple words a normal vector can tell were the surface is faced. As expected surfaces that are faced towards the light source, will be brighter than ones that face in other directions.

In order to calculate the normals vector, the cross product is used because the cross product of two vectors A and B will give a vector perpendicular to both vectors A and B.

Page 14: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

14

Lighting: the Phong reflection model

Lighting is a very important part of a 3D scene because it adds realism. In the context of

this thesis the Phong reflection model is analyzed and by using shaders, applied to a WebGL scene. Real world objects are visible because they reflect light. This comes from light that is either reflected from other objects or surfaces or comes directly from a light source. Each object reacts differently to the incident light depending on the material of the object. Rough surfaces scatter the light in every direction (diffuse reflection) while smooth ones tend to reflect it in a mirror-like fashion (specular reflection). The Phong reflection model is a combination of the diffuse reflection, the specular reflection and also an effect of the ambient light. The Phong reflection model is very accurately represented by the following image.

The Ambient reflection comes from light scattered around the scene. This type of reflection is independent from any light source and its effect is the same for each fragment of the illuminated object.

The diffuse reflection effect is light source dependent which means that each fragment diffuses different amount of light depending on its position compared to the light source. Assuming that the light source is a vector the amount of diffused light is calculated by the cosine of the angle between the vector of the light source and the normal of this fragment. Instead of calculating the cosine, which is a very time consuming operation, if both vectors have a length of one, the dot product of the two vectors can be used. Luckily even if a vector has not length of one it can be converted to such a vector via an operation called normalizing. Normalizing a vector involves two steps: 1 calculate its length, then, 2 divide each of its (xy or xyz) components by its length. Given vector a its xyz components are calculated as follows,

x = ax/|a| y = ay/|a|

Page 15: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

15

z = az/|a|

The specular reflection is mirror-like reflection of light from a surface, in which light from a single incoming direction is reflected into a single outgoing direction. The intensity of the reflected light that is visible depends on the surface it reflects on, and the relative position between the light source and the observer. When light hits a mirror the angle of the reflected light is the same as the angle of the incident light. Like the specular light the amount of the reflected light is proportional to the cosine of two vectors at this case the one is the vector of a ray that was reflected of the surface like it hit a mirror (from now on it will be called R) and the other is the vector pointing in the observer’s direction (from now on called V). Of course in terms with calculations economy the dot product is used in this case again. So this far the observer’s point of view and the position of the light source have played their part in the calculation the only remaining is the material. In order to take under consideration this factor the dot product of (normalized) R and V is exponentiated to a constant describing the material’s shininess α

Per-Fragment Phong implementation

The per-fragment term means that the light effect is calculated for each pixel separately. This means that the calculations are done by the fragment shader. Here follows an implementation of the per-fragment Phong reflection model fragment shader. <script id="shader-fs" type="x-shader/x-fragment">

precision mediump float;

varying vec2 vTextureCoord;

varying vec3 vTransformedNormal;

varying vec4 vPosition;

uniform float uMaterialShininess;

uniform bool uUseTextures;

uniform vec3 uAmbientColor;

uniform vec3 uPointLightingLocation;

uniform vec3 uPointLightingSpecularColor;

uniform vec3 uPointLightingDiffuseColor;

uniform sampler2D uSampler;

Page 16: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

16

void main(void) {

vec3 lightWeighting;

//Calculate the light source’s direction

vec3 lightDirection = normalize(uPointLightingLocation -

vPosition.xyz);

vec3 normal = normalize(vTransformedNormal);

//Calculate how much specular reflection the viewers eye can see

float specularLightWeighting = 0.0;

vec3 eyeDirection = normalize(-vPosition.xyz);

vec3 reflectionDirection = reflect(-lightDirection, normal);

specularLightWeighting = pow(max(dot(reflectionDirection,

eyeDirection), 0.0), uMaterialShininess);

//Calculate the diffuse light

float diffuseLightWeighting = max(dot(normal, lightDirection),

0.0);

//Calculate the aggregate effect

lightWeighting = uAmbientColor

+ uPointLightingSpecularColor * specularLightWeighting

+ uPointLightingDiffuseColor * diffuseLightWeighting;

vec4 fragmentColor;

//Find out the color of the fragment before it is illuminated

fragmentColor = texture2D(uSampler, vec2(vTextureCoord.s,

vTextureCoord.t));

//Apply the effect of the light on the fragment

gl_FragColor = vec4(fragmentColor.rgb * lightWeighting,

fragmentColor.a);

}

</script>

Page 17: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

17

The viewing transformations

In order to draw an object contained in the buffers to the screen it needs to go through a series of conversions. These conversions translate the 3D coordinates contained in the buffers to the 2D world that is the screen. Until computer screens are replaced by 3D holograms this procedure is essential so that the viewer can perceive the essence of a 3D scene through a 2D means. The process of coordinate conversion begins by converting the world coordinate system to the eye coordinate system. This is done by the model and view transformations. The first defines the object’s position in the scene and the other transforms the orientation of the world to the orientation of the viewer’s eye. The second step of the conversion is to transform the eye coordinate system to the canonical screen space this is implemented by the projection transformation. The projection transformation has two different versions the perspective and the orthographic projection. The first takes into consideration the fact that objects in a distance are smaller than objects closer to the viewer’s eye while the second does not. This property makes the perspective projection suitable for drawing 3D scenes where depth maters such as a room, and the orthographic projection suitable to show profile, or precise measurements of a 3D object such as architectural plans. The final step of the conversion is to transform the canonical screen space to the viewport which in WebGL’s case is the canvas. This means that the canonical screen space is set to fit to the exact size that the canvas was set to during initialization.

In WebGL, the matrices that perform the transformations are the model-view matrix and the

projection matrix. The viewport transformation is done by the viewport function.

World Coordinate System

Eye Coordinate System

Canonical Screen Space

Model and View

Transformations

Projection

Transformation

Viewport

Transformation

Canvas

Page 18: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

18

gl.viewport(minX, minY, width, height);

Because WebGL has no implementation of array as a native data type, JavaScript arrays are

used and because JavaScript does not have methods implementing the linear algebra

operations needed for the transformation, in this thesis I used a third-party matrix library

Brandon Jones‘s, glMatrix. glMatrix offers a method that applies the perspective projection

transformation

mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1,

1000.0, pMatrix);

The first argument is the angle of the vertical field of view, the second is the aspect ratio of the

canvas the third is the nearest bound of the viewport and the fourth the far end. The last is the

destination matrix.

In every rendering cycle the model-view and the projection matrices need to be uploaded to the

shaders

function setMatrixUniforms() {

gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false,

pMatrix);

gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false,

mvMatrix);

var normalMatrix = mat3.create();

mat4.toInverseMat3(mvMatrix, normalMatrix);

mat3.transpose(normalMatrix);

gl.uniformMatrix3fv(shaderProgram.nMatrixUniform, false,

normalMatrix);

}

The first two lines pass as uniform variables the mvMatrix and the pMatrix. When vertices are

transformed, normal vectors should also be transformed in order to remain perpendicular. The

correct Normal matrix is obtained by transposing the inverse of the Model-View matrix. This

Normal matrix is used (in the vertex shader) to multiply the normal vectors in order to remain

perpendicular to the surface during a transformation.

vTransformedNormal = uNMatrix * aVertexNormal;

Page 19: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

19

Movement

Moving objects around is what gives action to a scene. Moving objects can be achieved

by mathematical transformations on the model-view matrix. The problem is that the model-view

matrix contains all the objects of a scene and in order to move only a subset of them matrix

stacks must be used to make sure that any modification affect only the object that needs to be

moved and not anything else.

Moving an object requires three steps. The first step is to save (pushMatrix) the current

model-view matrix to a stack. The second step is to apply the transformation, by multiplying the

original model-view matrix by the transformation matrix that represents the respective

operation (rotation, translation, scale). The new model-view matrix needs to be passed to the

program (shaders) and then draw it using either the drawArrays or the drawElements

functions. The third step is to recover (popMatrix) the original model-view matrix from the

stack.

Here follows the implementation of pushMatrix and popMatrix using the glMatrix library

function mvPushMatrix() {

var copy = mat4.create();

mat4.set(mvMatrix, copy);

mvMatrixStack.push(copy);

}

function mvPopMatrix() {

if (mvMatrixStack.length == 0) {

throw "Invalid popMatrix!";

}

mvMatrix = mvMatrixStack.pop();

}

The procedure described above moves only one object and performs only one movement

for example it can rotate an object for some degrees. In order to see motion this procedure

needs to be called several times every second. Just like animated films the scene is drawn every

time with the object that needs to be moved, in a slightly different position. If this happens in a

rate greater than thirty scenes every second, the human eye cannot distinct each different

scene an sees a smoothly moving object.

In WebGL the way to achieve rendering a scene in such a frame rate is to use a loop that executes an iteration every given time interval. During an iteration there is time to calculate the position of each object and render the scene. In order to control the time interval of each

iteration, JavaScript offers the requestAnimFrame function. Because so far each browser has

a different name for this function here follows an elegant solution by Google.

Page 20: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

20

requestAnimFrame = (function() {

return window.requestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.mozRequestAnimationFrame ||

window.oRequestAnimationFrame ||

window.msRequestAnimationFrame ||

function(/* function FrameRequestCallback */ callback, /* DOMElement

Element */ element) {

window.setTimeout(callback, 1000/60);

};

})();

This function is called every 1000/60 milliseconds providing a very good frame rate for the loop.

function mainLoop() {

requestAnimFrame(mainLoop);

drawScene();

animate();

}

Textures

Textures are bitmap images that are applied to the surface of a 2D or 3D object in order to add

detail.

In order to use an image as a texture in WebGL it needs to be loaded

Texture = gl.createTexture();

Texture.image = new Image();

Texture.image.onload = function ()

{

handleLoadedTexture(Texture)

}

Texture.image.src = "texture.jpg";

At this point the variable Texture has been initialized as a WebGL texture and an additional

attribute is added to this variable that holds the image. After the image has been loaded, is

time to initialize the texture so it can be used. First of all the texture needs to be flipped

vertically because the texture’s coordinate system and the image’s coordinate system are

opposite on the vertical axis. Texture coordinates follow the Cartesian system, the y parameter

increases as the y axis moves away from the x axis while images are encoded in the opposite

way.

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

Page 21: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

21

In WebGL textures are manipulated like the buffers. Just as in order to perform any operation

on a buffer it needs to be set as the current buffer, textures need to be set as the current

texture in order to use them.

gl.bindTexture(gl.TEXTURE_2D, texture);

As expected textures need to be uploaded to the graphics card. This is done by the following

function call

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,

texture.image);

The first parameter defines what kind of image is uploaded, the second is the level of detail

level 0 is the base image level. The two following parameters specify the format in which the

texture is loaded in the graphics card. The next specifies the data type of the data and the last

the image itself.

The following step is to specify some scaling parameters of the texture. These parameters

configure how the texture is scaled up or scaled down when this is necessary.

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,

gl.LINEAR_MIPMAP_NEAREST);

The gl.TEXTURE_MAG_FILTER argument sets the texture magnification method to either

gl.NEAREST or gl.LINEAR. The first returns the value of the texture element that is nearest

to the center of the pixel being textured. The second returns the weighted average of the four

texture elements that are closest to the center of the pixel being textured. Respectively the

gl.TEXTURE_MIN_FILTER argument sets the texture’s minifying method. There are six

methods the first two are the gl.NEAREST and gl.LINEAR which work exactly as mentioned

above. The other four use mipmaps. A mipmap is an ordered set of arrays representing the

same image at progressively lower resolutions.

gl.NEAREST_MIPMAP_NEAREST Chooses the

mipmap that most closely matches the size of

the pixel being textured and uses the NEAREST

criterion to produce a texture value.

gl.LINEAR_MIPMAP_NEAREST Chooses the

mipmap that most closely matches the size of

the pixel being textured and uses the LINEAR

criterion to produce a texture value.

gl.NEAREST_MIPMAP_LINEAR Chooses the

Page 22: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

22

two mipmaps that most closely match the size of the pixel being textured and uses the

NEAREST criterion to produce a texture value from each mipmap. The final texture value is a

weighted average of those two values.

gl.LINEAR_MIPMAP_LINEAR Chooses the two mipmaps that most closely match the size of

the pixel being textured and uses the LINEAR criterion to produce a texture value from each

mipmap. The final texture value is a weighted average of those two values.

In order to map a texture to a 3D object, coordinates are required. Every vertex in a

polygon is assigned a texture coordinate. Texture coordinates are usually automatically

generated with the 3D object by appropriate software and a little further down they will be

loaded from a Collada object so I won’t mention further about UV mapping.

Textures are applied on a surface by the shaders so the texture coordinates need to be

uploaded to the program. Texture coordinates are uploaded to the vertex shader by an

attribute attribute vec2 aVertexTextureCoords; Except the coordinates the program

needs the texture itself. Shaders access texture data by a data type called sampler2D. The

sampler is used by the fragment shader so it is a uniform uniform sampler2D uSampler;

WebGL can handle up to 32 textures every rendering cycle and are numbers from

TEXTURE0 to TEXTURE31 so the active texture needs to be specified. Also the sampler needs

to be associated with the current texture.

gl.activeTexture(gl.TEXTURE0);

gl.bindTexture(gl.TEXTURE_2D, texture);

gl.uniform1i(Program.uSampler, 0);

At this point the shaders can use the textures to define the color of each fragment

fragmentColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));

Because the texture coordinates are passed as attribute in the vertex shader the

vTextureCoord is passed as a varying variable from the vertex to the fragment shader.

Page 23: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

23

Loading a Collada 3D object

Collada is an XML-based format for interchanging 3D data between applications. Collada’s structure is huge and very complicated and making an XML parser to load a 3D Collada object is a very demanding task. For this reason I used the Collada loader from the KickJS game engine. KickJS is a WebGL based game engine build for modern web browsers such as the most recent version of Chrome and Firefox. The source code is Open Source (BSD New License) and the source code is available on GitHub. At this point I should mention that despite the fact that KickJS is a complete game engine, I only used the Collada loader and not any other part related to rendering or lighting. In order to use the KickJS the following steps need to be followed

1. Download the source code from GitHub 2. Build the project. Windows users need to download Cygwin in order to be able execute

Unix shell commands from their system. After downloading and installing Cygwin locate the Cygwin directory (usually C:\cygwin) inside this directory Cygwin is able to execute Unix shell commands nowhere else in the windows file system. For this reason copy the KickJS source code in the Cygwin/home/user directory, then download and install nodejs also in this folder as well as the Google Clojure Compiler. After that move into the folder that contains the KickJS source code and make sure that there is the build.sh script and then open the Cygwin console and move in this directory. Then run the following command sh build.sh ../nodejs/node . ../GoogleClojure/compiler.jar

../nodejs/node

If the build was successful there must be a folder named “build” and inside it two JavaScript files kick-min-0.4.0 and Kick-debug-0.4.0 one folder named “pre” containing multiple JavaScript files.

3. In order to embed the KickJS project to another project copy the folder created in step 2

and paste it wherever in the project’s folder is necessary 4. Include the kick-min-0.4.0.js or the kick-debug-0.4.0.js as long as every JavaScript file

necessary from the “pre” folder (I recommend to include them all because there are many dependencies among them.) <script type="text/javascript" src="src/kick-min-

0.4.0.js"></script>

<script src="src/pre/chunk.js"></script>

<script src="src/pre/collada.js"></script>

<script src="src/pre/constants.js"></script>

<script src="src/pre/core.js"></script>

<script src="src/pre/glslconstants.js"></script>

Page 24: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

24

<script src="src/pre/material.js"></script>

<script src="src/pre/math.js"></script>

<script src="src/pre/mesh.js"></script>

<script src="src/pre/meshfactory.js"></script>

<script src="src/pre/obj.js"></script>

<script src="src/pre/resource.js"></script>

<script src="src/pre/scene.js"></script>

<script src="src/pre/texture.js"></script>

At this point the KickJS components can be used and of course the Collada loader. As mentioned earlier Collada is an XML schema so in order to parse XML content from HTML, an XMLHttpRequest is necessary. The XMLHttpRequest loads the XML content so that it is available from the DOM interface. function loadCollada(url)

{

var oReq = new XMLHttpRequest();

oReq.open("GET", url, false);

oReq.onreadystatechange = handler;

oReq.send();

function handler()

{

if (oReq.readyState == 4 /* complete */)

{

console.log( oReq.status );

var xmlDom = oReq.responseXML;

Load(xmlDom,url,false);

}

}

}

The oReq variable is the request, the open method states that a GET request will be executed

on the given url (url is the general term in this case a path is used.) the false argument states

that the loading should be done synchronously. After the loading is done the Load function is

called. function Load(content,url,rotateAroundX){

var core = KICK.namespace("KICK.core"),

importer = KICK.namespace("KICK.importer");

var engine = new

KICK.core.Engine(document.getElementById("canvas"),{enableDebugContext

: false});

createdObject =

importer.ColladaImporter.import(content,engine,engine.activeScene,rota

teAroundX);

Page 25: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

25

Because KickJS uses a way of simulating namespaces in JavaScript, in order to use the respective engine components they need to be used in this manner. The important part here is the import function which is KickJS’s Collada loader. This function returns a JavaScript object that contains the following attributes.

Attribute Name Data Type Description

vertex Float Vec3 The vertex coordinates

normal Float Vec3 The normal of each vertex

tangent Float Vec4 The tangent of each vertex

uv1 Float Vec2 The primary texture coordinates

uv2 Float Vec2 The secondary texture coordinates

int1 … int4 Integer Vec1-4 Generic integer fields of size (1,2,3,4)

color Float Vec4 Vertex colour

Using the respective fields the WebGL buffers are populated during initialization. vertices = createdObject.mesh[0].meshData.vertex;

Mesh = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, Mesh);

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),

gl.STATIC_DRAW);

Page 26: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

26

Conclusion - Future plans

This thesis is showing only some of WebGL’s basic components. WebGL is capable for much more and since JavaScript is getting faster by the day the road is open for large scale applications to be developed. 3D games, Augmented or Virtual Reality applications so far could not be imagined to run through a web browser but with WebGL this is not an unlikely scenario.

Page 27: Real-time shader-based Per-fragment lighting for dynamic ...€¦ · εφαρμογών για μοντέρνους φυλλομετρητές (web browsers). Σε αυτήν την

27

References 1. WebGL beginner’s guide Diego Cantor, Brandon Jones Pact Publishing 2012 2. www.learningwebgl.com 3. Interactive Computer Graphics - A Top-Down Appr. 6th ed. - E. Angel, et al., (Pearson,

2012) BBS 4. https://www.khronos.org/registry/webgl/specs/1.0/ 5. http://en.wikipedia.org/wiki/WebGL 6. http://en.wikipedia.org/wiki/Canvas_element 7. http://en.wikipedia.org/wiki/Texture_mapping 8. http://en.wikipedia.org/wiki/UV_mapping 9. http://en.wikipedia.org/wiki/Shaders 10. http://www.khronos.org/registry/webgl/specs/latest/ 11. http://en.wikipedia.org/wiki/XML 12. http://www.opengl.org/sdk/docs/man/xhtml/glTexParameter.xml 13. http://www.kickjs.org/ 14.

http://mortennobel.files.wordpress.com/2012/03/webgl-based-game-engine1.pdf