1/*
2 * Copyright (C) 2009 Apple Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26//
27// initWebGL
28//
29// Initialize the Canvas element with the passed name as a WebGL object and return the
30// WebGLRenderingContext.
31function initWebGL(canvasName, vshader, fshader, attribs, clearColor, clearDepth)
32{
33    var canvas = document.getElementById(canvasName);
34    return gl = WebGLUtils.setupWebGL(canvas);
35}
36
37function log(msg) {
38    if (window.console && window.console.log) {
39        window.console.log(msg);
40    }
41}
42
43// Load shaders with the passed names and create a program with them. Return this program
44// in the 'program' property of the returned context.
45//
46// For each string in the passed attribs array, bind an attrib with that name at that index.
47// Once the attribs are bound, link the program and then use it.
48//
49// Set the clear color to the passed array (4 values) and set the clear depth to the passed value.
50// Enable depth testing and blending with a blend func of (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
51//
52// A console function is added to the context: console(string). This can be replaced
53// by the caller. By default, it maps to the window.console() function on WebKit and to
54// an empty function on other browsers.
55//
56function simpleSetup(gl, vshader, fshader, attribs, clearColor, clearDepth)
57{
58    // create our shaders
59    var vertexShader = loadShader(gl, vshader);
60    var fragmentShader = loadShader(gl, fshader);
61
62    // Create the program object
63    var program = gl.createProgram();
64
65    // Attach our two shaders to the program
66    gl.attachShader (program, vertexShader);
67    gl.attachShader (program, fragmentShader);
68
69    // Bind attributes
70    for (var i = 0; i < attribs.length; ++i)
71        gl.bindAttribLocation (program, i, attribs[i]);
72
73    // Link the program
74    gl.linkProgram(program);
75
76    // Check the link status
77    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
78    if (!linked && !gl.isContextLost()) {
79        // something went wrong with the link
80        var error = gl.getProgramInfoLog (program);
81        log("Error in program linking:"+error);
82
83        gl.deleteProgram(program);
84        gl.deleteProgram(fragmentShader);
85        gl.deleteProgram(vertexShader);
86
87        return null;
88    }
89
90    gl.useProgram(program);
91
92    gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
93    gl.clearDepth(clearDepth);
94
95    gl.enable(gl.DEPTH_TEST);
96    gl.enable(gl.BLEND);
97    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
98
99    return program;
100}
101
102//
103// loadShader
104//
105// 'shaderId' is the id of a <script> element containing the shader source string.
106// Load this shader and return the WebGLShader object corresponding to it.
107//
108function loadShader(ctx, shaderId)
109{
110    var shaderScript = document.getElementById(shaderId);
111    if (!shaderScript) {
112        log("*** Error: shader script '"+shaderId+"' not found");
113        return null;
114    }
115
116    if (shaderScript.type == "x-shader/x-vertex")
117        var shaderType = ctx.VERTEX_SHADER;
118    else if (shaderScript.type == "x-shader/x-fragment")
119        var shaderType = ctx.FRAGMENT_SHADER;
120    else {
121        log("*** Error: shader script '"+shaderId+"' of undefined type '"+shaderScript.type+"'");
122        return null;
123    }
124
125    // Create the shader object
126    var shader = ctx.createShader(shaderType);
127
128    // Load the shader source
129    ctx.shaderSource(shader, shaderScript.text);
130
131    // Compile the shader
132    ctx.compileShader(shader);
133
134    // Check the compile status
135    var compiled = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
136    if (!compiled && !ctx.isContextLost()) {
137        // Something went wrong during compilation; get the error
138        var error = ctx.getShaderInfoLog(shader);
139        log("*** Error compiling shader '"+shaderId+"':"+error);
140        ctx.deleteShader(shader);
141        return null;
142    }
143
144    return shader;
145}
146
147//
148// makeBox
149//
150// Create a box with vertices, normals and texCoords. Create VBOs for each as well as the index array.
151// Return an object with the following properties:
152//
153//  normalObject        WebGLBuffer object for normals
154//  texCoordObject      WebGLBuffer object for texCoords
155//  vertexObject        WebGLBuffer object for vertices
156//  indexObject         WebGLBuffer object for indices
157//  numIndices          The number of indices in the indexObject
158//
159function makeBox(ctx)
160{
161    // box
162    //    v6----- v5
163    //   /|      /|
164    //  v1------v0|
165    //  | |     | |
166    //  | |v7---|-|v4
167    //  |/      |/
168    //  v2------v3
169    //
170    // vertex coords array
171    var vertices = new Float32Array(
172        [  1, 1, 1,  -1, 1, 1,  -1,-1, 1,   1,-1, 1,    // v0-v1-v2-v3 front
173           1, 1, 1,   1,-1, 1,   1,-1,-1,   1, 1,-1,    // v0-v3-v4-v5 right
174           1, 1, 1,   1, 1,-1,  -1, 1,-1,  -1, 1, 1,    // v0-v5-v6-v1 top
175          -1, 1, 1,  -1, 1,-1,  -1,-1,-1,  -1,-1, 1,    // v1-v6-v7-v2 left
176          -1,-1,-1,   1,-1,-1,   1,-1, 1,  -1,-1, 1,    // v7-v4-v3-v2 bottom
177           1,-1,-1,  -1,-1,-1,  -1, 1,-1,   1, 1,-1 ]   // v4-v7-v6-v5 back
178    );
179
180    // normal array
181    var normals = new Float32Array(
182        [  0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1,     // v0-v1-v2-v3 front
183           1, 0, 0,   1, 0, 0,   1, 0, 0,   1, 0, 0,     // v0-v3-v4-v5 right
184           0, 1, 0,   0, 1, 0,   0, 1, 0,   0, 1, 0,     // v0-v5-v6-v1 top
185          -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  -1, 0, 0,     // v1-v6-v7-v2 left
186           0,-1, 0,   0,-1, 0,   0,-1, 0,   0,-1, 0,     // v7-v4-v3-v2 bottom
187           0, 0,-1,   0, 0,-1,   0, 0,-1,   0, 0,-1 ]    // v4-v7-v6-v5 back
188       );
189
190
191    // texCoord array
192    var texCoords = new Float32Array(
193        [  1, 1,   0, 1,   0, 0,   1, 0,    // v0-v1-v2-v3 front
194           0, 1,   0, 0,   1, 0,   1, 1,    // v0-v3-v4-v5 right
195           1, 0,   1, 1,   0, 1,   0, 0,    // v0-v5-v6-v1 top
196           1, 1,   0, 1,   0, 0,   1, 0,    // v1-v6-v7-v2 left
197           0, 0,   1, 0,   1, 1,   0, 1,    // v7-v4-v3-v2 bottom
198           0, 0,   1, 0,   1, 1,   0, 1 ]   // v4-v7-v6-v5 back
199       );
200
201    // index array
202    var indices = new Uint8Array(
203        [  0, 1, 2,   0, 2, 3,    // front
204           4, 5, 6,   4, 6, 7,    // right
205           8, 9,10,   8,10,11,    // top
206          12,13,14,  12,14,15,    // left
207          16,17,18,  16,18,19,    // bottom
208          20,21,22,  20,22,23 ]   // back
209      );
210
211    var retval = { };
212
213    retval.normalObject = ctx.createBuffer();
214    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.normalObject);
215    ctx.bufferData(ctx.ARRAY_BUFFER, normals, ctx.STATIC_DRAW);
216
217    retval.texCoordObject = ctx.createBuffer();
218    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.texCoordObject);
219    ctx.bufferData(ctx.ARRAY_BUFFER, texCoords, ctx.STATIC_DRAW);
220
221    retval.vertexObject = ctx.createBuffer();
222    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.vertexObject);
223    ctx.bufferData(ctx.ARRAY_BUFFER, vertices, ctx.STATIC_DRAW);
224
225    ctx.bindBuffer(ctx.ARRAY_BUFFER, null);
226
227    retval.indexObject = ctx.createBuffer();
228    ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, retval.indexObject);
229    ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, indices, ctx.STATIC_DRAW);
230    ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null);
231
232    retval.numIndices = indices.length;
233
234    return retval;
235}
236
237//
238// makeSphere
239//
240// Create a sphere with the passed number of latitude and longitude bands and the passed radius.
241// Sphere has vertices, normals and texCoords. Create VBOs for each as well as the index array.
242// Return an object with the following properties:
243//
244//  normalObject        WebGLBuffer object for normals
245//  texCoordObject      WebGLBuffer object for texCoords
246//  vertexObject        WebGLBuffer object for vertices
247//  indexObject         WebGLBuffer object for indices
248//  numIndices          The number of indices in the indexObject
249//
250function makeSphere(ctx, radius, lats, longs)
251{
252    var geometryData = [ ];
253    var normalData = [ ];
254    var texCoordData = [ ];
255    var indexData = [ ];
256
257    for (var latNumber = 0; latNumber <= lats; ++latNumber) {
258        for (var longNumber = 0; longNumber <= longs; ++longNumber) {
259            var theta = latNumber * Math.PI / lats;
260            var phi = longNumber * 2 * Math.PI / longs;
261            var sinTheta = Math.sin(theta);
262            var sinPhi = Math.sin(phi);
263            var cosTheta = Math.cos(theta);
264            var cosPhi = Math.cos(phi);
265
266            var x = cosPhi * sinTheta;
267            var y = cosTheta;
268            var z = sinPhi * sinTheta;
269            var u = 1-(longNumber/longs);
270            var v = latNumber/lats;
271
272            normalData.push(x);
273            normalData.push(y);
274            normalData.push(z);
275            texCoordData.push(u);
276            texCoordData.push(v);
277            geometryData.push(radius * x);
278            geometryData.push(radius * y);
279            geometryData.push(radius * z);
280        }
281    }
282
283    for (var latNumber = 0; latNumber < lats; ++latNumber) {
284        for (var longNumber = 0; longNumber < longs; ++longNumber) {
285            var first = (latNumber * (longs+1)) + longNumber;
286            var second = first + longs + 1;
287            indexData.push(first);
288            indexData.push(second);
289            indexData.push(first+1);
290
291            indexData.push(second);
292            indexData.push(second+1);
293            indexData.push(first+1);
294        }
295    }
296
297    var retval = { };
298
299    retval.normalObject = ctx.createBuffer();
300    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.normalObject);
301    ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(normalData), ctx.STATIC_DRAW);
302
303    retval.texCoordObject = ctx.createBuffer();
304    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.texCoordObject);
305    ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(texCoordData), ctx.STATIC_DRAW);
306
307    retval.vertexObject = ctx.createBuffer();
308    ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.vertexObject);
309    ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(geometryData), ctx.STATIC_DRAW);
310
311    retval.numIndices = indexData.length;
312    retval.indexObject = ctx.createBuffer();
313    ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, retval.indexObject);
314    ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData), ctx.STREAM_DRAW);
315
316    return retval;
317}
318
319// Array of Objects curently loading
320var g_loadingObjects = [];
321
322// Clears all the Objects currently loading.
323// This is used to handle context lost events.
324function clearLoadingObjects() {
325    for (var ii = 0; ii < g_loadingObjects.length; ++ii) {
326        g_loadingObjects[ii].onreadystatechange = undefined;
327    }
328    g_loadingObjects = [];
329}
330
331//
332// loadObj
333//
334// Load a .obj file from the passed URL. Return an object with a 'loaded' property set to false.
335// When the object load is complete, the 'loaded' property becomes true and the following
336// properties are set:
337//
338//  normalObject        WebGLBuffer object for normals
339//  texCoordObject      WebGLBuffer object for texCoords
340//  vertexObject        WebGLBuffer object for vertices
341//  indexObject         WebGLBuffer object for indices
342//  numIndices          The number of indices in the indexObject
343//
344function loadObj(ctx, url)
345{
346    var obj = { loaded : false };
347    obj.ctx = ctx;
348    var req = new XMLHttpRequest();
349    req.obj = obj;
350    g_loadingObjects.push(req);
351    req.onreadystatechange = function () { processLoadObj(req) };
352    req.open("GET", url, true);
353    req.send(null);
354    return obj;
355}
356
357function processLoadObj(req)
358{
359    log("req="+req)
360    // only if req shows "complete"
361    if (req.readyState == 4) {
362        g_loadingObjects.splice(g_loadingObjects.indexOf(req), 1);
363        doLoadObj(req.obj, req.responseText);
364    }
365}
366
367function doLoadObj(obj, text)
368{
369    vertexArray = [ ];
370    normalArray = [ ];
371    textureArray = [ ];
372    indexArray = [ ];
373
374    var vertex = [ ];
375    var normal = [ ];
376    var texture = [ ];
377    var facemap = { };
378    var index = 0;
379
380    // This is a map which associates a range of indices with a name
381    // The name comes from the 'g' tag (of the form "g NAME"). Indices
382    // are part of one group until another 'g' tag is seen. If any indices
383    // come before a 'g' tag, it is given the group name "_unnamed"
384    // 'group' is an object whose property names are the group name and
385    // whose value is a 2 element array with [<first index>, <num indices>]
386    var groups = { };
387    var currentGroup = [-1, 0];
388    groups["_unnamed"] = currentGroup;
389
390    var lines = text.split("\n");
391    for (var lineIndex in lines) {
392        var line = lines[lineIndex].replace(/[ \t]+/g, " ").replace(/\s\s*$/, "");
393
394        // ignore comments
395        if (line[0] == "#")
396            continue;
397
398        var array = line.split(" ");
399        if (array[0] == "g") {
400            // new group
401            currentGroup = [indexArray.length, 0];
402            groups[array[1]] = currentGroup;
403        }
404        else if (array[0] == "v") {
405            // vertex
406            vertex.push(parseFloat(array[1]));
407            vertex.push(parseFloat(array[2]));
408            vertex.push(parseFloat(array[3]));
409        }
410        else if (array[0] == "vt") {
411            // normal
412            texture.push(parseFloat(array[1]));
413            texture.push(parseFloat(array[2]));
414        }
415        else if (array[0] == "vn") {
416            // normal
417            normal.push(parseFloat(array[1]));
418            normal.push(parseFloat(array[2]));
419            normal.push(parseFloat(array[3]));
420        }
421        else if (array[0] == "f") {
422            // face
423            if (array.length != 4) {
424                log("*** Error: face '"+line+"' not handled");
425                continue;
426            }
427
428            for (var i = 1; i < 4; ++i) {
429                if (!(array[i] in facemap)) {
430                    // add a new entry to the map and arrays
431                    var f = array[i].split("/");
432                    var vtx, nor, tex;
433
434                    if (f.length == 1) {
435                        vtx = parseInt(f[0]) - 1;
436                        nor = vtx;
437                        tex = vtx;
438                    }
439                    else if (f.length = 3) {
440                        vtx = parseInt(f[0]) - 1;
441                        tex = parseInt(f[1]) - 1;
442                        nor = parseInt(f[2]) - 1;
443                    }
444                    else {
445                        obj.ctx.console.log("*** Error: did not understand face '"+array[i]+"'");
446                        return null;
447                    }
448
449                    // do the vertices
450                    var x = 0;
451                    var y = 0;
452                    var z = 0;
453                    if (vtx * 3 + 2 < vertex.length) {
454                        x = vertex[vtx*3];
455                        y = vertex[vtx*3+1];
456                        z = vertex[vtx*3+2];
457                    }
458                    vertexArray.push(x);
459                    vertexArray.push(y);
460                    vertexArray.push(z);
461
462                    // do the textures
463                    x = 0;
464                    y = 0;
465                    if (tex * 2 + 1 < texture.length) {
466                        x = texture[tex*2];
467                        y = texture[tex*2+1];
468                    }
469                    textureArray.push(x);
470                    textureArray.push(y);
471
472                    // do the normals
473                    x = 0;
474                    y = 0;
475                    z = 1;
476                    if (nor * 3 + 2 < normal.length) {
477                        x = normal[nor*3];
478                        y = normal[nor*3+1];
479                        z = normal[nor*3+2];
480                    }
481                    normalArray.push(x);
482                    normalArray.push(y);
483                    normalArray.push(z);
484
485                    facemap[array[i]] = index++;
486                }
487
488                indexArray.push(facemap[array[i]]);
489                currentGroup[1]++;
490            }
491        }
492    }
493
494    // set the VBOs
495    obj.normalObject = obj.ctx.createBuffer();
496    obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.normalObject);
497    obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(normalArray), obj.ctx.STATIC_DRAW);
498
499    obj.texCoordObject = obj.ctx.createBuffer();
500    obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.texCoordObject);
501    obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(textureArray), obj.ctx.STATIC_DRAW);
502
503    obj.vertexObject = obj.ctx.createBuffer();
504    obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.vertexObject);
505    obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(vertexArray), obj.ctx.STATIC_DRAW);
506
507    obj.numIndices = indexArray.length;
508    obj.indexObject = obj.ctx.createBuffer();
509    obj.ctx.bindBuffer(obj.ctx.ELEMENT_ARRAY_BUFFER, obj.indexObject);
510    obj.ctx.bufferData(obj.ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexArray), obj.ctx.STREAM_DRAW);
511
512    obj.groups = groups;
513
514    obj.loaded = true;
515}
516
517// Array of images curently loading
518var g_loadingImages = [];
519
520// Clears all the images currently loading.
521// This is used to handle context lost events.
522function clearLoadingImages() {
523    for (var ii = 0; ii < g_loadingImages.length; ++ii) {
524        g_loadingImages[ii].onload = undefined;
525    }
526    g_loadingImages = [];
527}
528
529//
530// loadImageTexture
531//
532// Load the image at the passed url, place it in a new WebGLTexture object and return the WebGLTexture.
533//
534function loadImageTexture(ctx, url)
535{
536    var texture = ctx.createTexture();
537    ctx.bindTexture(ctx.TEXTURE_2D, texture);
538    ctx.texImage2D(ctx.TEXTURE_2D, 0, ctx.RGBA, 1, 1, 0, ctx.RGBA, ctx.UNSIGNED_BYTE, null);
539    var image = new Image();
540    g_loadingImages.push(image);
541    image.onload = function() { doLoadImageTexture(ctx, image, texture) }
542    image.src = url;
543    return texture;
544}
545
546function doLoadImageTexture(ctx, image, texture)
547{
548    g_loadingImages.splice(g_loadingImages.indexOf(image), 1);
549    ctx.bindTexture(ctx.TEXTURE_2D, texture);
550    ctx.texImage2D(
551        ctx.TEXTURE_2D, 0, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE, image);
552    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR);
553    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR);
554    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
555    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
556    //ctx.generateMipmap(ctx.TEXTURE_2D)
557    ctx.bindTexture(ctx.TEXTURE_2D, null);
558}
559
560//
561// Framerate object
562//
563// This object keeps track of framerate and displays it as the innerHTML text of the
564// HTML element with the passed id. Once created you call snapshot at the end
565// of every rendering cycle. Every 500ms the framerate is updated in the HTML element.
566//
567Framerate = function(id)
568{
569    this.numFramerates = 10;
570    this.framerateUpdateInterval = 500;
571    this.id = id;
572
573    this.renderTime = -1;
574    this.framerates = [ ];
575    self = this;
576    var fr = function() { self.updateFramerate() }
577    setInterval(fr, this.framerateUpdateInterval);
578}
579
580Framerate.prototype.updateFramerate = function()
581{
582    var tot = 0;
583    for (var i = 0; i < this.framerates.length; ++i)
584        tot += this.framerates[i];
585
586    var framerate = tot / this.framerates.length;
587    framerate = Math.round(framerate);
588    document.getElementById(this.id).innerHTML = "Framerate:"+framerate+"fps";
589}
590
591Framerate.prototype.snapshot = function()
592{
593    if (this.renderTime < 0)
594        this.renderTime = new Date().getTime();
595    else {
596        var newTime = new Date().getTime();
597        var t = newTime - this.renderTime;
598        if (t == 0)
599            return;
600        var framerate = 1000/t;
601        this.framerates.push(framerate);
602        while (this.framerates.length > this.numFramerates)
603            this.framerates.shift();
604        this.renderTime = newTime;
605    }
606}
607