1 /* -*- mode: javascript; tab-width: 4; indent-tabs-mode: nil; -*- */ 2 /** 3 * 4 * @file Let's Make a Demo Workshop 1.x base library 5 * 6 */ 7 8 /** 9 * 10 * <h3>A Dirty Small Library for Graphics</h3> 11 * 12 * <p>This was, and basically still is, course material for our course 13 * "TIEA311 Principles of Computer graphics" (based on "6.837 Computer 14 * Graphics" as published in MIT OCW). Am I a rogue teacher leaking 15 * example answers here? No. I'm quite certain that a student who 16 * manages to translate this convoluted Javascript snippet to 17 * grade-worthy C++ exercise answers has earned the credit, probably 18 * the "hard way", too. 19 * </p> 20 * 21 * <p>This code does dirty tricks to fit in very small storage space as 22 * if it was part of an entry in a demoscene "4k intro" 23 * competition. That means at most 4096 bytes for everything. Also, 24 * this is written by a total Javascript newbie - proper practices of 25 * any language can only be learned by lots and lots of programming 26 * and reading codes by more experienced programmers. So far, I've 27 * only gone through some tutorial examples, parts of specifications, 28 * and demoscene intro codes that necessarily minimize code size with 29 * the expense of *everything* else. 30 * </p> 31 * 32 * <p>Do not try these programming practices at home (until you know what 33 * you do and which parts of the specification are beautifully 34 * misused). 35 * </p> 36 * 37 * <p>If you're less experienced, try to keep on learning good 38 * programming practices (in Javascript and other languages) from nice 39 * tutorials. Listen to your teachers and coaches. They know what is 40 * best for you. That said, some of this code may show you some 41 * features that are less common in basic tutorials. 42 * </p> 43 * 44 * <p>Browser support: Productions made using this code seem to load and 45 * run in desktop Chrome, Edge, and Firefox, but not on IE. Mobile 46 * browsers unknown. 47 * </p> 48 * 49 * <p>Original goal: use at least spline curves, generalized cylinder, 50 * surface of revolution, hierarchical model, perspective projection, 51 * and simple fragment shading with directed light. (some 50% of 52 * course content) 53 * </p> 54 * 55 * <p>Outcome: yep, I got all that stuffed in an example production 56 * (along with shaders, vector math, softsynth, soundtrack, and an 57 * English message.. all in 4k "executable" after some serious 58 * minification). 59 * </p> 60 * 61 * 62 **/ 63 64 // Variables used all over the library. Must have as globals: 65 var gl, prg; 66 67 // TODO: Should these gl and prg be included as method parameters or 68 // object properties? 69 70 // -------------------------------------------------------------------------------- 71 // "Globals needed in many routines", these are used by the main starter code: 72 var C, Cw, Ch; // Canvas object and previous width and height 73 var audio; // Audio object needed for song playback 74 var s; // Temporary variable for "style" but also other things 75 var _document=document; // automatically minified name for the "document" object 76 77 // Global for camera inside scenegraph. 78 var cameraTransformation; 79 80 81 82 // Constants 83 var PI=Math.PI; 84 // var PI=3.141593 85 86 // Utility functions 87 function clamp01(parameter){ 88 if (parameter < 0) return 0; 89 if (parameter > 1) return 1; 90 return parameter; 91 } 92 93 // Observe: Everything looks transposed compared to the theory 94 // slides and C++ codes of the course. This is because of column 95 // major ordering used by Javascript arrays and the WebGL 96 // interface. The mathematical meaning is the same as on the 97 // course, but you should remember that what looks like a row here 98 // is actually a column if written as actual math. Pen and paper 99 // and your own brains are very powerful tools, as I keep 100 // repeating on my lectures. 101 102 // Bezier basis matrix 103 var bezB = [ 1, 0, 0, 0, 104 -3, 3, 0, 0, 105 3, -6, 3, 0, 106 -1, 3, -3, 1]; 107 108 // B-spline basis matrix 109 var bspB = [ 1./6, 4./6, 1./6, 0./6, 110 -3./6, 0./6, 3./6, 0./6, 111 3./6, -6./6, 3./6, 0./6, 112 -1./6, 3./6, -3./6, 1./6 ]; 113 114 // "Re-implementing the wheel" ------------------------------ 115 116 /** Returns a 4x4 matrix of zeros */ 117 function zeros(){ 118 return [ 0, 0, 0, 0, 119 0, 0, 0, 0, 120 0, 0, 0, 0, 121 0, 0, 0, 0 ]; 122 }; 123 124 /** Returns a translation matrix of homogeneous coordinates. */ 125 function translate(tx,ty,tz){ 126 return [ 1, 0, 0, 0, 127 0, 1, 0, 0, 128 0, 0, 1, 0, 129 tx, ty, tz, 1 ]; 130 }; 131 132 /** Returns a scale matrix. */ 133 function scaleXYZ(sx,sy,sz){ 134 return [ sx, 0, 0, 0, 135 0, sy, 0, 0, 136 0, 0, sz, 0, 137 0, 0, 0, 1 ]; 138 }; 139 140 /** Returns an isotropic scale matrix. */ 141 function scale(s){ 142 return scaleXYZ(s,s,s); 143 }; 144 145 /** Counter-clockwise rotation around the Z-axis */ 146 function rotZ(theta){ 147 var s = Math.sin(theta); 148 var c = Math.cos(theta); 149 return [ c, -s, 0, 0, 150 s, c, 0, 0, 151 0, 0, 1, 0, 152 0, 0, 0, 1]; 153 }; 154 155 /** Counter-clockwise rotation around the Y-axis */ 156 function rotY(theta){ 157 var s = Math.sin(theta); 158 var c = Math.cos(theta); 159 return [ c, 0, -s, 0, 160 0, 1, 0, 0, 161 s, 0, c, 0, 162 0, 0, 0, 1]; 163 }; 164 165 /** Counter-clockwise rotation around the X-axis */ 166 function rotX(theta){ 167 var s = Math.sin(theta); 168 var c = Math.cos(theta); 169 return [ 1, 0, 0, 0, 170 0, c, -s, 0, 171 0, s, c, 0, 172 0, 0, 0, 1]; 173 }; 174 175 /** Perspective projection, imitates gluPerspective() */ 176 function perspective(fov, ar, near, far) { 177 var f = 1/Math.tan(fov/2); 178 var div = near - far; 179 return [f/ar, 0, 0, 0, 180 0, f, 0, 0, 181 0, 0, (far+near)/div, -1, 182 0, 0, 2*far*near/div, 0 ]; 183 }; 184 185 /** 186 * Perspective projection as in gluPerspective() but assumes a 187 * precomputed f==1/Math.tan(fov/2) 188 */ 189 function perspectiveF(f, ar, near, far) { 190 var div = near - far; 191 return [f/ar, 0, 0, 0, 192 0, f, 0, 0, 193 0, 0, (far+near)/div, -1, 194 0, 0, 2*far*near/div, 0 ]; 195 }; 196 197 /** 198 * Perspective with hardcoded near plane "quite near". Far plane 199 * is "far away". Might have issues with Z stability. Assumes 200 * precomputed f==1/Math.tan(fov/2). 201 */ 202 function perspectiveFhc(f, ar) { 203 return [f/ar, 0, 0, 0, 204 0, f, 0, 0, 205 0, 0, -1, -1, 206 0, 0, -1, 0 ]; 207 }; 208 209 210 /** 211 * Orthographic projection with fixed width 212 */ 213 function orthographic(top, ar, near, far) { 214 var bottom=-top,right=top*ar, left=-top*ar; 215 return [2/(right-left), 0, 0, 0, 216 0, 2/(top-bottom), 0, 0, 217 0, 0, -2/(far-near), 0, 218 -(right+left)/(right-left), 219 -(top+bottom)/(top-bottom), 220 -(far+near)/(far-near), 221 1 ]; 222 }; 223 224 // "Matrices with inverses" ------------------------------ 225 function translate_wi(tx,ty,tz){ 226 var res=translate(tx,ty,tz); 227 res.n=translate(-tx,-ty,-tz); 228 return res; 229 }; 230 /** Returns a scale matrix. */ 231 function scaleXYZ_wi(sx,sy,sz){ 232 var res=scaleXYZ(sx,sy,sz); 233 res.n=scaleXYZ(1/sx,1/sy,1/sz); 234 return res; 235 }; 236 /** Returns an isotropic scale matrix. */ 237 function scale_wi(s){ 238 return scaleXYZ_wi(s,s,s); 239 }; 240 function rotZ_wi(theta){ 241 var res=rotZ(theta); 242 res.n=rotZ(-theta); 243 return res; 244 } 245 function rotY_wi(theta){ 246 var res=rotY(theta); 247 res.n=rotY(-theta); 248 return res; 249 } 250 function rotX_wi(theta){ 251 var res=rotX(theta); 252 res.n=rotX(-theta); 253 return res; 254 } 255 256 257 258 259 260 /** 4x4 Matrix multiplication */ 261 function matmul(a,b){ 262 var i,j,k,m = zeros(); 263 for (i=0;i<4;i++){ 264 for (j=0;j<4;j++){ 265 for(k=0;k<4;k++){ 266 m[j*4+i] += a[k*4+i]*b[j*4+k]; 267 } 268 } 269 } 270 return m; 271 } 272 273 /** 4x4 Matrix times 4x1 vector multiplication */ 274 function matvec(a,b){ 275 var i,j,res = [0,0,0,0]; 276 for (i=0;i<4;i++){ 277 for(j=0;j<4;j++){ 278 res[i] += a[j*4+i]*b[j]; 279 } 280 } 281 return res; 282 } 283 284 /** 285 * 4x4 matrix times 4xN matrix multiplication. Does a bit of extra 286 * work but allows the same routine to multiply both matrices and 287 * vectors. The end result seems to be 15 bytes shorter when using 288 * this instead of separate matmul and matvec routines. 289 */ 290 function matmul4(a,b){ 291 var i,j,k,m=[]; 292 for (i=0;i<b.length;i++){ 293 m[i]=0; 294 } 295 for (i=0;i<4;i++){ 296 for (j=0;j<(b.length/4);j++){ 297 for(k=0;k<4;k++){ 298 m[j*4+i] += a[k*4+i]*b[j*4+k]; 299 } 300 } 301 } 302 return m; 303 } 304 305 /** 306 * For 4x4 matrices, multiplies both a*b and b^{-1} * a^{-1}. 307 */ 308 function matmul_wi(a,b){ 309 var res = matmul4(a,b); 310 res.n = matmul4(b.n,a.n); 311 return res; 312 } 313 314 /** 315 * Transpose the upper 3x3 part and zero the rest. For building 316 * the normal matrix from the inverse of model matrix. 317 */ 318 function transposed3x3(a){ 319 return [a[0],a[4],a[8], 0, 320 a[1],a[5],a[9], 0, 321 a[2],a[6],a[10],0, 322 0, 0, 0, 1]; 323 } 324 325 /** Cross product for homogeneous directions. "[(axb)^t,0]^t" */ 326 function cross(a,b){ 327 return [a[1]*b[2]-a[2]*b[1], 328 a[2]*b[0]-a[0]*b[2], 329 a[0]*b[1]-a[1]*b[0], 330 0]; 331 } 332 333 /** Normalize x,y,z disregarding and untouching w */ 334 function nmld(v){ 335 var length3d=Math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); 336 return [v[0]/length3d,v[1]/length3d,v[2]/length3d,v[3]]; 337 } 338 339 /** Homogenize by x/w,y/w,z/w */ 340 function homogenized(v){ 341 if (v[3]==0) return v; 342 return [v[0]/v[3],v[1]/v[3],v[2]/v[3],1]; 343 } 344 345 346 // "Prelude to re-inventions" ------------------------------ 347 348 // Tentative material object.. 349 // colors==[a,d,s,q] could be a 4x4 matrix? 350 // Yep, quite short code when colors are in a matrix. 351 // TODO: I think need to learn the object model of javascript, to know 352 // if this is proper. 353 function Material(colors){ 354 var myc=colors.slice(); 355 this.c = function(gl){ 356 gl.uniformMatrix4fv( 357 gl.getUniformLocation(prg,"i"), false, myc); 358 } 359 } 360 361 // Tentative camera object.. just put inverse of current transf in the 362 // global cameraTransformation var. 363 function Camera(){ 364 this.c = function(nodetrans){ 365 //alert("Positioning camera" + nodetrans); 366 cameraTransformation = nodetrans.n; 367 cameraTransformation.n = nodetrans; 368 369 } 370 } 371 372 373 374 // Traversal without inverse matrices and thus normal matrices. Use 375 // this if you know you don't need normals or camera as scenegraph node. 376 function traverse(node,ms){ 377 ms=node.f.reduce(matmul4,ms); 378 gl.uniformMatrix4fv( 379 gl.getUniformLocation(prg,"mv"), false, ms); 380 node.o.map(function(o){o.c(gl);}); // map < forEach :) 381 node.c.map(function(c){traverse(c,ms);}); 382 } 383 384 /** 385 * Traverse a scene graph, and output to the WebGL pipeline. 386 * 387 * Algorithm: 388 * 389 * This is a very basic recursive tree traversal. 390 * 391 * Size optimizations: 392 * 393 * - The 'functional' operations provided by Javascript allow a very 394 * compact way of writing this. 395 * 396 * - Call map() without using its output. It is wasteful, but it is 397 * 4 characters shorter to write than forEach(). 398 * 399 * - Always send two uniform matrices, regardless of whether 400 * anything is drawn using them. Wasted effort when not drawing. 401 **/ 402 function traverse_wi(node,ms){ 403 ms=node.f.reduce(matmul_wi,ms); 404 gl.uniformMatrix4fv( 405 gl.getUniformLocation(prg,"mv"), false, ms); 406 gl.uniformMatrix4fv( 407 gl.getUniformLocation(prg,"nm"), false, transposed3x3(ms.n)); 408 node.o.map(function(o){o.c(gl);}); // map < forEach :) 409 node.c.map(function(c){traverse_wi(c,ms);}); 410 } 411 412 /** Dry traversal to find camera (TODO: lights?) 413 * 414 * This traverses the scene graph but doesn't draw anything. Look for 415 * property 'r' (for cameRa or dRy or Rehearsal or whatever 416 * mnemonic..) and apply the functions found in the property. 417 */ 418 function findcam_wi(node,ms){ 419 ms = node.f.reduce(matmul_wi,ms); 420 if (node.r){ 421 node.r.map(function(o){o.c(ms);}); 422 } 423 node.c.map(function(c){findcam_wi(c,ms);}); 424 } 425 426 427 428 429 430 431 // "Re-inventing the cylinder" ------------------------------ 432 433 /** 434 * Create an evaluator that can return a local frame for circle in 435 * the xy plane. As in the MIT course assignment, but transposed. 436 */ 437 function funCircleBasic(radius,n){ 438 var r = radius; 439 this.n = n; // Fidelity hint for the surface evaluator 440 this.c = function(t){ 441 var s = Math.sin(t * 2 * PI); 442 var c = Math.cos(t * 2 * PI); 443 return [-c, -s, 0, 0, // normal 444 0, 0, 1, 0, // binormal 445 -s, c, 0, 0, // tangent 446 c*r, s*r, 0, 1 // position 447 ]; 448 }; 449 } 450 451 /** 452 * Create an evaluator that can return a local frame for a part or 453 * whole circle on the xy plane. The starting point of the arc is 454 * at the highest y point "top", and direction is counterclockwise 455 * in right-handed coordinates, z towards viewer. Assumes 456 * 0<arclen<=1. Arclen is optional. 457 */ 458 function funCircle(radius,swpfidel,arclen){ 459 var r = radius; 460 var a = arclen?arclen:1; 461 // Fidelity hint for the sweep surface evaluator 462 this.n = swpfidel?swpfidel:10; 463 this.c = function(t){ 464 var s = Math.sin(t * 2 * PI * a); 465 var c = Math.cos(t * 2 * PI * a); 466 return [ s, -c, 0, 0, // normal 467 0, 0, 1, 0, // binormal 468 -c, -s, 0, 0, // tangent 469 -s*r, c*r, 0, 1 // position 470 ]; 471 }; 472 } 473 474 475 // Line from zero to length, along y axis 476 // Some bugs here? Haven't actually used this at all... 477 function funLine(length,n){ 478 var l = length; 479 this.n = n; 480 this.c = function(t){ 481 return [ 1, 0, 0, 0, // "normal"/orientation 482 0, 0, 1, 0, // binormal 483 0, 1, 0, 0, // tangent 484 0, t*l, 0, 1 // position 485 ]; 486 }; 487 } 488 489 490 /** 491 * Simple uniform B-spline evaluator. 492 * 493 * Hmm.. position and tangent could be evaluated for any t... But 494 * how to maintain correct normal and bi-normal for surface 495 * creation? Initial idea: pre-compute at some intervals using the 496 * cross product trick from our lecture notes, and then evaluate a 497 * normalized interpolant upon call to eval(). NOTE: Only need to 498 * store binormal (?), because normal can be computed via cross 499 * product. The binormal rotations could be corrected while 500 * precomputing. (not yet done). 501 * 502 * Note that analytic tangent doesn't exist for all possible 503 * inputs - we don't handle curves with vanishing derivatives, 504 * so keep this in mind when defining control points. 505 * 506 * TODO: Maybe could be sloppy and not even interpolate, if result 507 * is OK visually? Hmm... we do have storage, so why not just 508 * precompute like a *lot* of values when creating the spline 509 * object, and then return the closest one in compute(t)? Nasty, but 510 * without redundance... 511 * 512 * Back-of-the-envelope: 100 control pts * 100 intermediates * 16 513 * * 8 byte float is 1280000 bytes... well.. that's a megabyte for 514 * one spline.. admittedly, it sounds like a lot.. 515 * 516 * TODO (cheating a bit, though): Use only xy-curves with no 517 * change of curvature, and delete all the code about flipping 518 * gradients. Would be so much smaller and leaner! Well.. if we 519 * don't cheat that much, then at least the production should USE 520 * the feature and have some twisty curve(s) to show it 521 * off.. Closure will omit unused functions, so we can have many 522 * versions here in the library. This is the bloaty, most general 523 * version. Depending on what you need, you can "cheat" more by 524 * using the restricted versions below. 525 */ 526 527 function funBSpline(pts) { 528 // Let us declare all vars here to shorten the code. 529 var i; 530 var g,Tt,dTt,v,T,N; 531 var t,ifirst,npts = this.n = pts.length/4; 532 var nsteps = (npts-3)*300; // internal points (actually +1 because we go to t=1.0) 533 var b = []; // the internal storage 534 var B = [0,0,1,0]; // "arbitrary binormal at beginning" 535 //this.n=npts; 536 537 for (i=0;i<nsteps+1;i++){ 538 t = i/nsteps; // scale t to [0,1] within curve 539 if (i<nsteps) { 540 ifirst = t*(npts-3) | 0; // 1st point is... (funny "|0" makes a floor()) 541 t = t*(npts-3) - ifirst; // reset t to [0,1] within segm. 542 } else { 543 ifirst = npts-4; 544 t = 1; 545 } 546 547 g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G. 548 Tt = [1, t, t*t, t*t*t]; 549 dTt = [0, 1, 2*t, 3*t*t]; 550 v = matmul4(matmul4(g, bspB),Tt); 551 T = nmld(matmul4(matmul4(g, bspB),dTt)); 552 N = nmld(cross(B,T)); 553 B = nmld(cross(T,N)); 554 b.push([].concat(N, // "normal"/orientation 555 B, // binormal 556 T, // tangent 557 v)); // pos. 558 } 559 560 // as of now, we don't care to interpolate: 561 this.c = function(t){ 562 return b[0 | t*(nsteps)]; 563 } 564 } 565 566 /** 567 * Simple uniform B-spline with transformed control points.. This 568 * was "new" in Instanssi 2018 - allows scaled and/or skewed 569 * geometries but needs to be done during instantiation. 570 */ 571 function funBSplineTransformed(pts,tfm) { 572 // Let us declare all vars here to shorten the code. 573 var i; 574 var g,Tt,dTt,v,T,N; 575 var t,ifirst,npts = this.n = pts.length/4; 576 var nsteps = (npts-3)*300; // internal points (actually +1 because we go to t=1.0) 577 var b = []; // the internal storage 578 var B = [0,0,1,0]; // "arbitrary binormal at beginning" 579 //this.n=npts; 580 pts = matmul4(tfm,pts); // transform! 581 582 583 for (i=0;i<nsteps+1;i++){ 584 t = i/nsteps; // scale t to [0,1] within curve 585 if (i<nsteps) { 586 ifirst = t*(npts-3) | 0; // 1st point is... (funny "|0" makes a floor()) 587 t = t*(npts-3) - ifirst; // reset t to [0,1] within segm. 588 } else { 589 ifirst = npts-4; 590 t = 1; 591 } 592 593 g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G. 594 Tt = [1, t, t*t, t*t*t]; 595 dTt = [0, 1, 2*t, 3*t*t]; 596 v = matmul4(matmul4(g, bspB),Tt); 597 T = nmld(matmul4(matmul4(g, bspB),dTt)); 598 N = nmld(cross(B,T)); 599 B = nmld(cross(T,N)); 600 b.push([].concat(N, // "normal"/orientation 601 B, // binormal 602 T, // tangent 603 v)); // pos. 604 } 605 606 // as of now, we don't care to interpolate: 607 this.c = function(t){ 608 return b[0 | t*(nsteps)]; 609 } 610 } 611 612 613 614 /** 615 * XY-plane curves with no inflection points (B==(0,0,1)). 616 * 617 * Some 58 bytes smaller than the more general version. 618 * 619 */ 620 function funBSplineXYnoInf(ipts) { 621 // Let us declare all vars here to shorten the code. 622 var npts = this.n = ipts.length/4; 623 var pts=ipts; 624 625 this.c = function(t){ 626 var ifirst; 627 // t arrives as [0,1] within curve 628 if (t<1) { 629 ifirst = t*(npts-3) | 0; 630 t = t*(npts-3) - ifirst; 631 } else { 632 ifirst = npts-4; 633 //t = 1; 634 } 635 636 var g = pts.slice(ifirst*4,ifirst*4+16), // pick cps to G. 637 B = [0,0,1,0], // Constant binormal 638 Tt = [1, t, t*t, t*t*t], 639 dTt = [0, 1, 2*t, 3*t*t], 640 T = nmld(matmul4(matmul4(g, bspB),dTt)); 641 return [].concat(nmld(cross(B,T)), // "normal"/orientation 642 B, // binormal 643 T, // tangent 644 matmul4(matmul4(g, bspB),Tt)); // pos. 645 } 646 } 647 648 649 /** 650 * Simple rational Bezier evaluator. 651 * 652 */ 653 654 function funBezierCurve(pts) { 655 // Let us declare all vars here to shorten the code. 656 var i; 657 var g,Tt,dTt,v,T,N; 658 var t,ifirst,npts = pts.length/4; 659 this.n = pts.length*4; // Fidelity hint for surface eval 660 var nsteps = ((npts-1)/3)*10; // internal points (actually +1 because we go to t=1.0) 661 var b = []; // the internal storage 662 var B = [0,0,1,0]; // "arbitrary binormal at beginning" 663 //this.n=npts; 664 665 for (i=0;i<nsteps+1;i++){ 666 t = i/nsteps; // scale t to [0,1] within curve 667 if (i<nsteps) { 668 ifirst = (t*((npts-1)/3) | 0)*3; // 1st point is... (funny "|0" makes a floor()) 669 t = t*((npts-1)/3) - (ifirst/3); // reset t to [0,1] within segm. 670 } else { 671 ifirst = npts-4; 672 t = 1; 673 } 674 675 g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G. 676 Tt = [1, t, t*t, t*t*t]; 677 dTt = [0, 1, 2*t, 3*t*t]; 678 //v = matmul4(matmul4(g, bezB),Tt); 679 v = homogenized(matmul4(matmul4(g, bezB),Tt)); 680 T = nmld(matmul4(matmul4(g, bezB),dTt)); 681 N = nmld(cross(B,T)); 682 B = nmld(cross(T,N)); 683 b.push([].concat(N, // "normal"/orientation 684 B, // binormal 685 T, // tangent 686 v)); // pos. 687 //console.log("ja tuota " + npts + " " + ifirst + " " + t); 688 } 689 690 // as of now, we don't care to interpolate: 691 this.c = function(t){ 692 return b[0 | t*(nsteps)]; 693 } 694 } 695 696 697 698 /** 699 * Convert packed 2d-vectors to (x,y,0,1) homogenous 3d 700 * coordinates. (Abandoned idea - didn't gain anything) 701 */ 702 function xyToHomog(ptsxy){ 703 var res=[]; 704 for(var i=0;i<ptsxy.length;){ 705 res.push(ptsxy[i++],ptsxy[i++],0,1); 706 } 707 return res; 708 } 709 710 /** 711 * Push values of the matrix column icol to array, optionally 712 * multiply by mul!=0 713 */ 714 function pushCol4(array,mat,icol,mul){ 715 mul=mul?mul:1; 716 for(var i=0;i<4;i++){ 717 array.push(mul*mat[4*icol+i]); 718 } 719 } 720 721 /** Helper function. More bloaty than the few inlined calls: */ 722 function createAndFillArrayBuffer32(data){ 723 var buf=gl.createBuffer(); 724 gl.bindBuffer(gl.ARRAY_BUFFER, buf); 725 gl.bufferData(gl.ARRAY_BUFFER, 726 new Float32Array(data), 727 gl.STATIC_DRAW); 728 return buf; 729 } 730 731 /** 732 * Prepare a drawable WebGL buffer that represents a box. 733 * 734 * It's a bit funny that even though the box is a very simple 735 * shape, the data and code required for making one is actually 736 * quite bloaty when compared to generalized cylinders. Hmm, 737 * technically it could be possible to make a box with a 4-point 738 * circle, but then there should be some clever way to make 739 * non-smooth normals. Think about this.. 740 * 741 */ 742 function Box(size){ 743 // GL buffer objects 744 var vertexColorBuf = gl.createBuffer(), 745 vertexBuf = gl.createBuffer(), 746 vertexNormalBuf = gl.createBuffer(), 747 faceBuf = gl.createBuffer(); 748 749 var vertices=[]; 750 var normals=[]; 751 var vind=[]; 752 753 vertices = [ 754 // Front 755 -1, -1, 1, 1, 756 1, -1, 1, 1, 757 1, 1, 1, 1, 758 -1, 1, 1, 1, 759 // Back 760 -1, -1, -1, 1, 761 -1, 1, -1, 1, 762 1, 1, -1, 1, 763 1, -1, -1, 1, 764 // Top 765 -1, 1, -1, 1, 766 -1, 1, 1, 1, 767 1, 1, 1, 1, 768 1, 1, -1, 1, 769 // Bottom 770 -1, -1, -1, 1, 771 1, -1, -1, 1, 772 1, -1, 1, 1, 773 -1, -1, 1, 1, 774 // Right 775 1, -1, -1, 1, 776 1, 1, -1, 1, 777 1, 1, 1, 1, 778 1, -1, 1, 1, 779 // Left 780 -1, -1, -1, 1, 781 -1, -1, 1, 1, 782 -1, 1, 1, 1, 783 -1, 1, -1, 1, 784 ]; 785 786 normals = [ 787 // Front 788 0, 0, 1, 0, 789 0, 0, 1, 0, 790 0, 0, 1, 0, 791 0, 0, 1, 0, 792 // Back 793 0, 0,-1, 0, 794 0, 0,-1, 0, 795 0, 0,-1, 0, 796 0, 0,-1, 0, 797 // Top 798 0, 1, 0, 0, 799 0, 1, 0, 0, 800 0, 1, 0, 0, 801 0, 1, 0, 0, 802 // Bottom 803 0,-1, 0, 0, 804 0,-1, 0, 0, 805 0,-1, 0, 0, 806 0,-1, 0, 0, 807 // Right face 808 1, 0, 0, 0, 809 1, 0, 0, 0, 810 1, 0, 0, 0, 811 1, 0, 0, 0, 812 // Left face 813 -1, 0, 0, 0, 814 -1, 0, 0, 0, 815 -1, 0, 0, 0, 816 -1, 0, 0, 0, 817 ] 818 819 /* 820 // Colors.. nah.. 821 gcolors = [ 822 // Front 823 1, 0, 0, 1, 824 0, 1, 0, 1, 825 0, 0, 1, 1, 826 0, 1, 0, 1, 827 // Back 828 -1, -1, -1, 1, 829 -1, 1, -1, 1, 830 1, 1, -1, 1, 831 1, -1, -1, 1, 832 // Top 833 -1, 1, -1, 1, 834 -1, 1, 1, 1, 835 1, 1, 1, 1, 836 1, 1, -1, 1, 837 // Bottom 838 -1, -1, -1, 1, 839 1, -1, -1, 1, 840 1, -1, 1, 1, 841 -1, -1, 1, 1, 842 // Right 843 1, -1, -1, 1, 844 1, 1, -1, 1, 845 1, 1, 1, 1, 846 1, -1, 1, 1, 847 // Left 848 -1, -1, -1, 1, 849 -1, -1, 1, 1, 850 -1, 1, 1, 1, 851 -1, 1, -1, 1, 852 ]; 853 */ 854 855 vind = [ 856 0, 1, 2, 0, 2, 3, // front 857 4, 5, 6, 4, 6, 7, // back 858 8, 9, 10, 8, 10, 11, // top 859 12, 13, 14, 12, 14, 15, // bottom 860 16, 17, 18, 16, 18, 19, // right 861 20, 21, 22, 20, 22, 23, // left 862 ]; 863 864 // Fill in buffers (non-animated shapes) 865 866 //hmm? 867 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 868 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); 869 870 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 871 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 872 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 873 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); 874 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 875 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW); 876 877 // "Compute", i.e., Bind and draw to pipeline 878 this.c = function(gl){ 879 var i; 880 // This quite unnecessary, actually (TODO:) 881 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 882 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g")); 883 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 884 885 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 886 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v")); 887 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 888 889 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 890 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N")); 891 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 892 893 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 894 gl.drawElements(gl.TRIANGLES, 6*2*3, gl.UNSIGNED_SHORT,0); 895 }; 896 897 898 } 899 900 901 /** 902 * A square [-1,1]^2 on xy-plane, z==0. This may not be very useful.. 903 */ 904 function Square(size){ 905 // GL buffer objects 906 var vertexColorBuf = gl.createBuffer(), 907 vertexBuf = gl.createBuffer(), 908 vertexNormalBuf = gl.createBuffer(), 909 faceBuf = gl.createBuffer(); 910 911 var vertices=[]; 912 var normals=[]; 913 var vind=[]; 914 915 vertices = [ 916 // Front 917 -1, -1, 0, 1, 918 1, -1, 0, 1, 919 1, 1, 0, 1, 920 -1, 1, 0, 1, 921 ]; 922 923 normals = [ 924 // Front 925 0, 0, 1, 0, 926 0, 0, 1, 0, 927 0, 0, 1, 0, 928 0, 0, 1, 0, 929 ] 930 931 vind = [ 932 0, 1, 2, 0, 2, 3, 933 ]; 934 935 // Fill in buffers (non-animated shapes) 936 937 //hmm? 938 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 939 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); 940 941 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 942 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 943 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 944 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); 945 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 946 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW); 947 948 // "Compute", i.e., Bind and draw to pipeline 949 this.c = function(gl){ 950 var i; 951 // This quite unnecessary, actually (TODO:) 952 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 953 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g")); 954 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 955 956 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 957 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v")); 958 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 959 960 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 961 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N")); 962 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 963 964 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 965 gl.drawElements(gl.TRIANGLES, 1*2*3, gl.UNSIGNED_SHORT,0); 966 }; 967 } 968 969 970 971 972 973 /** 974 * Prepare a drawable WebGL buffer of a generalized cylinder 975 * (swept profile + caps) 976 */ 977 function GenCyl(prof, profilesz, swp) { 978 var j,i,t,fp,lf,fs,ts; 979 var vertices = []; 980 var normals = []; 981 var vind = []; 982 var colors = []; 983 //var numfaces = (profilesz-1)*(sweepsz-1)*2+2*profilesz; 984 var sweepsz = (swp.n*1.5)|0; // heuristic for sweep size. 985 986 //var numfaces = 2*(sweepsz)*(profilesz-1)+2; 987 var numfaces = 2*(sweepsz-1)*(profilesz-1); 988 989 990 // GL buffer objects, to be filled as the last stage of init 991 var vertexColorBuf = gl.createBuffer(), 992 vertexNormalBuf = gl.createBuffer(), 993 vertexBuf = gl.createBuffer(), 994 faceBuf = gl.createBuffer(); 995 996 997 // location, normal, and color of each vertex 998 for (j=0; j<sweepsz; j++){ 999 ts = j/(sweepsz-1); 1000 fs = swp.c(ts); 1001 for (i=0;i<profilesz;i++){ 1002 t = i/(profilesz-1); 1003 fp = prof.c(t); 1004 lf = matmul4(fs,fp); 1005 //vertices.push(lf[12],lf[13],lf[14],1); 1006 //normals.push(lf[0],lf[1],lf[2],0); 1007 pushCol4(vertices,lf,3); 1008 pushCol4(normals,lf,0,-1); // invert! 1009 colors.push((2*j+i+0)%3?0:1, 1010 (2*j+i+1)%3?0:1, 1011 (2*j+i+2)%3?0:1, 1012 1); 1013 } 1014 } 1015 1016 // triangles as indices 1017 for (j=0; j<sweepsz-1; j++){ 1018 for (i=0; i<profilesz-1; i++){ 1019 vind.push(j*profilesz+i, j*profilesz+i+1, (j+1)*profilesz+i); 1020 vind.push(j*profilesz+i+1, (j+1)*profilesz+i+1, (j+1)*profilesz+i); 1021 } 1022 } 1023 1024 1025 // Add end caps (quite naive, assume convex profile curve 1026 // containing origin and curving to the left on xy-plane) 1027 // TODO: Separate, optional, function for making caps if needed. 1028 1029 /* 1030 // start cap (normal opposite of sweep tangent) 1031 fs = swp.c(0); // center point 1032 pushCol4(vertices,fs,3); 1033 pushCol4(normals,fs,2,-1); 1034 colors.push(1,0,0,1); 1035 for (i=0;i<profilesz;i++){ 1036 t = i/(profilesz-1); 1037 fp = prof.c(t); 1038 lf = matmul4(fs,fp); 1039 colors.push(0,i%2,(i+1)%2,1); 1040 pushCol4(vertices,lf,3); 1041 pushCol4(normals,fs,2,-1); 1042 } 1043 1044 // end cap (normal same as sweep tangent) 1045 fs = swp.c(1); // center point 1046 pushCol4(vertices,fs,3); 1047 pushCol4(normals,fs,2); 1048 colors.push(1,0,0,1); 1049 for (i=0;i<profilesz;i++){ 1050 t = i/(profilesz-1); 1051 fp = prof.c(t); 1052 lf = matmul4(fs,fp); 1053 colors.push(0,i%2,(i+1)%2,1); 1054 pushCol4(vertices,lf,3); 1055 pushCol4(normals,fs,2); 1056 1057 // Add faces here to avoid having another loop 1058 vind.push(sweepsz*profilesz, 1059 1+sweepsz*profilesz+((i+1)%profilesz), 1060 1+sweepsz*profilesz+i, 1061 1+sweepsz*profilesz+profilesz, 1062 1+sweepsz*profilesz+i+profilesz+1, 1063 1+sweepsz*profilesz+((i+1)%profilesz)+profilesz+1 1064 ); 1065 1066 } 1067 */ 1068 1069 // Fill in buffers (non-animated shapes) 1070 1071 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 1072 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 1073 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 1074 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 1075 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 1076 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); 1077 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 1078 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW); 1079 1080 // "Compute", i.e., Bind and draw to pipeline 1081 this.c = function(gl){ 1082 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf); 1083 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g")); 1084 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 1085 1086 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf); 1087 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v")); 1088 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 1089 1090 gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf); 1091 gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N")); 1092 gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); 1093 1094 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf); 1095 gl.drawElements(gl.TRIANGLES, numfaces*3, gl.UNSIGNED_SHORT,0); 1096 }; 1097 }; 1098 1099 1100 1101 1102