1 /*************************************** 2 * Copyright 2011, 2012 GlobWeb contributors. 3 * 4 * This file is part of GlobWeb. 5 * 6 * GlobWeb is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU Lesser General Public License as published by 8 * the Free Software Foundation, version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * GlobWeb is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU Lesser General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with GlobWeb. If not, see <http://www.gnu.org/licenses/>. 18 ***************************************/ 19 20 define( ['./BaseLayer', './Utils', './Program', './Mesh', './CoordinateSystem'], 21 function(BaseLayer, Utils, Program, Mesh, CoordinateSystem) { 22 23 /**************************************************************************************************************/ 24 25 /** 26 @constructor 27 Function constructor for EquatorialGridLayer 28 */ 29 var EquatorialGridLayer = function( options ) 30 { 31 BaseLayer.prototype.constructor.call( this, options ); 32 this.globe = null; 33 34 // Equatorial coordinates label renderables 35 this.labels = {}; 36 37 // WebGL textures 38 this.texturePool = null; 39 40 this.longitudeSample = options.longitudeSample || 15; // *24 = 360 41 this.latitudeSample = options.latitudeSample || 10; // *18 = 180 42 43 // Canvas for generation of equatorial coordinate labels 44 this.canvas2d = document.createElement("canvas"); 45 this.canvas2d.width = 100; 46 this.canvas2d.height = 20; 47 48 // Grid buffers 49 this.vertexBuffer = null; 50 this.indexBuffer = null; 51 } 52 53 /**************************************************************************************************************/ 54 55 Utils.inherits( BaseLayer, EquatorialGridLayer ); 56 57 /**************************************************************************************************************/ 58 59 /** 60 * Generate image data from text 61 * 62 * @param {String} text Text generated in canvas 63 */ 64 EquatorialGridLayer.prototype.generateImageData = function(text) 65 { 66 var ctx = this.canvas2d.getContext("2d"); 67 ctx.clearRect(0,0, this.canvas2d.width, this.canvas2d.height); 68 ctx.fillStyle = '#fff'; 69 ctx.font = '18px sans-serif'; 70 ctx.textBaseline = 'top'; 71 ctx.textAlign = 'center'; 72 var x = this.canvas2d.width / 2; 73 74 ctx.fillText(text, x, 0); 75 76 return ctx.getImageData(0,0, this.canvas2d.width,this.canvas2d.height); 77 } 78 79 /**************************************************************************************************************/ 80 81 /** 82 Attach the layer to the globe 83 */ 84 EquatorialGridLayer.prototype._attach = function( g ) 85 { 86 BaseLayer.prototype._attach.call( this, g ); 87 88 if ( this._visible ) 89 { 90 this.globe.tileManager.addPostRenderer(this); 91 } 92 93 if (!this.gridProgram) 94 { 95 var vertexShader = "\ 96 attribute vec3 vertex;\n\ 97 uniform mat4 viewProjectionMatrix;\n\ 98 void main(void) \n\ 99 {\n\ 100 gl_Position = viewProjectionMatrix * vec4(vertex, 1.0);\n\ 101 }\n\ 102 "; 103 104 var fragmentShader = "\ 105 precision highp float; \n\ 106 uniform float alpha; \n\ 107 void main(void)\n\ 108 {\n\ 109 gl_FragColor = vec4(1.0,1.0,1.0,alpha);\n\ 110 }\n\ 111 "; 112 113 var vertexTextShader = "\ 114 attribute vec3 vertex; // vertex have z = 0, spans in x,y from -0.5 to 0.5 \n\ 115 uniform mat4 viewProjectionMatrix; \n\ 116 uniform vec3 poiPosition; // world position \n\ 117 uniform vec2 poiScale; // x,y scale \n\ 118 \n\ 119 varying vec2 texCoord; \n\ 120 \n\ 121 void main(void) \n\ 122 { \n\ 123 // Generate texture coordinates, input vertex goes from -0.5 to 0.5 (on x,y) \n\ 124 texCoord = vertex.xy + vec2(0.5); \n\ 125 // Invert y \n\ 126 texCoord.y = 1.0 - texCoord.y; \n\ 127 \n\ 128 // Compute poi position in clip coordinate \n\ 129 gl_Position = viewProjectionMatrix * vec4(poiPosition, 1.0); \n\ 130 gl_Position.xy += vertex.xy * gl_Position.w * poiScale; \n\ 131 } \n\ 132 "; 133 134 var fragmentTextShader = "\ 135 #ifdef GL_ES \n\ 136 precision highp float; \n\ 137 #endif \n\ 138 \n\ 139 varying vec2 texCoord; \n\ 140 uniform sampler2D texture; \n\ 141 uniform float alpha; \n\ 142 \n\ 143 void main(void) \n\ 144 { \n\ 145 vec4 textureColor = texture2D(texture, texCoord); \n\ 146 gl_FragColor = vec4(textureColor.rgb, textureColor.a * alpha); \n\ 147 } \n\ 148 "; 149 150 this.gridProgram = new Program(this.globe.renderContext); 151 this.textProgram = new Program(this.globe.renderContext); 152 this.gridProgram.createFromSource( vertexShader, fragmentShader ); 153 this.textProgram.createFromSource( vertexTextShader, fragmentTextShader ); 154 } 155 156 // Texture used to show the equatorial coordinates 157 this.textMesh = new Mesh(this.globe.renderContext); 158 var vertices = [-0.5, -0.5, 0.0, 159 -0.5, 0.5, 0.0, 160 0.5, 0.5, 0.0, 161 0.5, -0.5, 0.0]; 162 var indices = [0, 3, 1, 1, 3, 2]; 163 this.textMesh.setVertices(vertices); 164 this.textMesh.setIndices(indices); 165 166 // Init grid buffers 167 var gl = this.globe.renderContext.gl; 168 this.vertexBuffer = gl.createBuffer(); 169 this.indexBuffer = gl.createBuffer(); 170 171 // Init texture pool 172 if ( !this.texturePool ) 173 this.texturePool = new TexturePool(gl); 174 } 175 176 /**************************************************************************************************************/ 177 178 /** 179 Detach the layer from the globe 180 */ 181 EquatorialGridLayer.prototype._detach = function() 182 { 183 var gl = this.globe.renderContext.gl; 184 gl.deleteBuffer( this.vertexBuffer ); 185 gl.deleteBuffer( this.indexBuffer ); 186 187 this.texturePool.disposeAll(); 188 for ( var i in this.labels ) 189 { 190 delete this.labels[i]; 191 } 192 193 this.globe.tileManager.removePostRenderer(this); 194 BaseLayer.prototype._detach.call(this); 195 196 } 197 198 /**************************************************************************************************************/ 199 200 /** 201 Render the grid 202 */ 203 EquatorialGridLayer.prototype.render = function( tiles ) 204 { 205 var renderContext = this.globe.renderContext; 206 var gl = renderContext.gl; 207 208 gl.disable(gl.DEPTH_TEST); 209 gl.enable(gl.BLEND); 210 gl.blendEquation(gl.FUNC_ADD); 211 gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 212 213 /*** Render grid ***/ 214 var geoBound = this.globe.getViewportGeoBound(); 215 this.computeSamples(geoBound) 216 this.generateGridBuffers(geoBound); 217 218 this.gridProgram.apply(); 219 mat4.multiply(renderContext.projectionMatrix, renderContext.viewMatrix, renderContext.modelViewMatrix) 220 gl.uniformMatrix4fv(this.gridProgram.uniforms["viewProjectionMatrix"], false, renderContext.modelViewMatrix); 221 gl.uniform1f(this.gridProgram.uniforms["alpha"], this._opacity ); 222 223 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 224 gl.vertexAttribPointer(this.gridProgram.attributes['vertex'], this.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0); 225 226 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); 227 gl.drawElements( gl.LINES, this.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 228 229 /*** Render label ***/ 230 this.generateText(geoBound); 231 this.textProgram.apply(); 232 233 mat4.multiply(renderContext.projectionMatrix, renderContext.viewMatrix, renderContext.modelViewMatrix) 234 gl.uniformMatrix4fv(this.textProgram.uniforms["viewProjectionMatrix"], false, renderContext.modelViewMatrix); 235 gl.uniform1i(this.textProgram.uniforms["texture"], 0); 236 237 var pixelSizeVector = renderContext.computePixelSizeVector(); 238 // for ( var n = 0; n < this.labels.length; n++ ) 239 for ( var n in this.labels ) 240 { 241 var label = this.labels[n]; 242 // Bind point texture 243 gl.activeTexture(gl.TEXTURE0); 244 gl.bindTexture(gl.TEXTURE_2D, label.texture); 245 246 // 2.0 * because normalized device coordinates goes from -1 to 1 247 var scale = [2.0 * label.textureWidth / renderContext.canvas.width, 248 2.0 * label.textureHeight / renderContext.canvas.height]; 249 250 gl.uniform2fv(this.textProgram.uniforms["poiScale"], scale); 251 // gl.uniform2fv(this.textProgram.uniforms["tst"], [ 0.5 / (label.textureWidth), 0.5 / (label.textureHeight) ]); 252 253 // Poi culling 254 var worldPoi = label.pos3d; 255 var poiVec = label.vertical; 256 scale = label.textureHeight * ( pixelSizeVector[0] * worldPoi[0] + pixelSizeVector[1] * worldPoi[1] + pixelSizeVector[2] * worldPoi[2] + pixelSizeVector[3] ); 257 258 var x = poiVec[0] * scale + worldPoi[0]; 259 var y = poiVec[1] * scale + worldPoi[1]; 260 var z = poiVec[2] * scale + worldPoi[2]; 261 262 gl.uniform3f(this.textProgram.uniforms["poiPosition"], x, y, z); 263 gl.uniform1f(this.textProgram.uniforms["alpha"], 1.); 264 265 this.textMesh.render(this.textProgram.attributes); 266 label.needed = false; 267 } 268 gl.enable(gl.DEPTH_TEST); 269 gl.disable(gl.BLEND); 270 } 271 272 /**************************************************************************************************************/ 273 274 /** 275 * Set visibility of the layer 276 */ 277 EquatorialGridLayer.prototype.visible = function( arg ) 278 { 279 if ( typeof arg == "boolean" && this._visible != arg ) 280 { 281 this._visible = arg; 282 283 if ( arg ) 284 { 285 this.globe.tileManager.addPostRenderer(this); 286 } 287 else 288 { 289 this.globe.tileManager.removePostRenderer(this); 290 } 291 } 292 293 return this._visible; 294 } 295 296 /**************************************************************************************************************/ 297 298 /** 299 * Set opacity of the layer 300 */ 301 EquatorialGridLayer.prototype.opacity = function( arg ) 302 { 303 return BaseLayer.prototype.opacity.call( this, arg ); 304 } 305 306 /**************************************************************************************************************/ 307 308 /** 309 * Compute samples depending on geoBound 310 */ 311 EquatorialGridLayer.prototype.computeSamples = function(geoBound) 312 { 313 var dlong = geoBound.east - geoBound.west; 314 var dlat = geoBound.north - geoBound.south; 315 316 // if under-sampled and not divergent 317 if ( dlong / this.longitudeSample < 3. && this.longitudeSample > 1. ) 318 { 319 this.longitudeSample /= 2; 320 this.latitudeSample /= 2; 321 } 322 323 // if over-sampled and not exceed the initial value 324 if ( dlong / this.longitudeSample > 7. && this.longitudeSample < 15. ) 325 { 326 this.longitudeSample *= 2; 327 this.latitudeSample *= 2; 328 } 329 } 330 331 /**************************************************************************************************************/ 332 333 /** 334 * Generate buffers object of the grid 335 */ 336 EquatorialGridLayer.prototype.generateGridBuffers = function(geoBound) 337 { 338 // Clamp min/max longitudes to sample 339 var west = (Math.floor(geoBound.west / this.longitudeSample))*this.longitudeSample; 340 var east = (Math.ceil(geoBound.east / this.longitudeSample))*this.longitudeSample; 341 342 var phiStart = Math.min( west, east ); 343 var phiStop = Math.max( west, east ); 344 345 // Difference is larger than hemisphere 346 if ( (east - west) > 180. ) 347 { 348 // pole in the viewport 349 phiStart = 0; 350 phiStop = 360; 351 } 352 else 353 { 354 phiStart = west; 355 phiStop = east; 356 } 357 358 359 // TODO adaptative generation of theta value 360 // for (var theta = geoBound.south; theta <= geoBound.north; theta+=latStep) { 361 362 var vertexPositionData = []; 363 var latitudeBands = 180. / this.latitudeSample; 364 365 for ( var latNumber = 0; latNumber <= latitudeBands; latNumber++ ) 366 { 367 var theta = latNumber * Math.PI / latitudeBands; 368 var sinTheta = Math.sin(theta); 369 var cosTheta = Math.cos(theta); 370 371 for ( var phi = phiStart; phi <= phiStop ; phi+=this.longitudeSample ) 372 { 373 var radPhi = phi * Math.PI / 180; 374 375 var sinPhi = Math.sin(radPhi); 376 var cosPhi = Math.cos(radPhi); 377 378 // z is the up vector 379 var x = cosPhi * sinTheta; 380 var y = sinPhi * sinTheta; 381 var z = cosTheta; 382 383 vertexPositionData.push(x); 384 vertexPositionData.push(y); 385 vertexPositionData.push(z); 386 } 387 } 388 389 var gl = this.globe.renderContext.gl; 390 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 391 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositionData), gl.STATIC_DRAW); 392 this.vertexBuffer.itemSize = 3; 393 this.vertexBuffer.numItems = vertexPositionData.length/3; 394 395 396 var indexData = []; 397 var longitudeBands = (phiStop - phiStart)/this.longitudeSample + 1; 398 399 for ( var latNumber = 0; latNumber < latitudeBands; latNumber++ ) 400 { 401 for ( var phi = phiStart, longNumber = 0; phi < phiStop ; phi+=this.longitudeSample, longNumber++ ) 402 { 403 var first = (latNumber * (longitudeBands)) + longNumber % (longitudeBands - 1); 404 var second = first + longitudeBands; 405 indexData.push(first); 406 indexData.push(first + 1); 407 408 indexData.push(first + 1); 409 indexData.push(second + 1); 410 411 indexData.push(second + 1); 412 indexData.push(second); 413 414 indexData.push(second); 415 indexData.push(first); 416 } 417 } 418 419 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); 420 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData), gl.STATIC_DRAW); 421 this.indexBuffer.itemSize = 1; 422 this.indexBuffer.numItems = indexData.length; 423 } 424 425 /**************************************************************************************************************/ 426 427 /** 428 * Generate text of the grid 429 */ 430 EquatorialGridLayer.prototype.generateText = function(geoBound) 431 { 432 // Clamp min/max longitudes to sample 433 var west = (Math.floor(geoBound.west / this.longitudeSample))*this.longitudeSample; 434 var east = (Math.ceil(geoBound.east / this.longitudeSample))*this.longitudeSample; 435 436 phiStart = Math.min( west, east ); 437 phiStop = Math.max( west, east ); 438 439 // Difference is larger than hemisphere 440 if ( (east - west) > 180. ) 441 { 442 // pole in the viewport => generate all longitude bands 443 // phiStart = east - 360; 444 // phiStop = west; 445 phiStart = 0; 446 phiStop = 360; 447 } 448 else 449 { 450 phiStart = west; 451 phiStop = east; 452 } 453 454 // Compute geographic position of center of canvas 455 var posX3d = this.globe.renderContext.get3DFromPixel( this.globe.renderContext.canvas.width / 2. , this.globe.renderContext.canvas.height / 2. ); 456 var posXgeo = []; 457 CoordinateSystem.from3DToGeo( posX3d, posXgeo ); 458 459 for ( var phi = phiStart; phi <= phiStop; phi+=this.longitudeSample ) 460 { 461 // convert to RA [0..360] 462 var RA = (phi < 0) ? phi+360 : phi; 463 var stringRA = CoordinateSystem.fromDegreesToHMS( RA ); 464 465 if ( !this.labels[stringRA] ) 466 { 467 this.labels[stringRA] = {}; 468 var imageData = this.generateImageData( stringRA ); 469 this._buildTextureFromImage(this.labels[stringRA],imageData); 470 } 471 472 // Compute position of label 473 var posGeo = [ phi, posXgeo[1] ]; 474 var pos3d = CoordinateSystem.fromGeoTo3D( posGeo ); 475 var vertical = vec3.create(); 476 vec3.normalize(pos3d, vertical); 477 478 this.labels[stringRA].pos3d = pos3d; 479 this.labels[stringRA].vertical = vertical; 480 this.labels[stringRA].needed = true; 481 } 482 483 // TODO <!> Adaptative rendering isn't totally implemented for theta due to difficulty to compute extrem latitude using geoBound <!> 484 var north = (Math.ceil(geoBound.north / this.latitudeSample))*this.latitudeSample; 485 var south = (Math.floor(geoBound.south / this.latitudeSample))*this.latitudeSample; 486 487 thetaStart = Math.min( north, south ); 488 thetaStop = Math.max( north, south ); 489 490 for ( var theta = thetaStart; theta <= thetaStop; theta+=this.latitudeSample ) 491 { 492 // for (var theta = -90; theta < 90; theta+=this.latitudeSample) { 493 494 var stringTheta = CoordinateSystem.fromDegreesToDMS( theta ); 495 if ( !this.labels[stringTheta] ) 496 { 497 this.labels[stringTheta] = {}; 498 var imageData = this.generateImageData( stringTheta ); 499 this._buildTextureFromImage(this.labels[stringTheta], imageData); 500 } 501 502 // Compute position of label 503 var posGeo = [ posXgeo[0], theta ]; 504 var pos3d = CoordinateSystem.fromGeoTo3D( posGeo ); 505 var vertical = vec3.create(); 506 vec3.normalize(pos3d, vertical); 507 508 this.labels[stringTheta].pos3d = pos3d; 509 this.labels[stringTheta].vertical = vertical; 510 this.labels[stringTheta].needed = true; 511 } 512 513 // Dispose texture if not needed 514 for ( var x in this.labels ) 515 { 516 if( !this.labels[x].needed ) 517 { 518 this.texturePool.disposeGLTexture(this.labels[x].texture); 519 delete this.labels[x]; 520 } 521 } 522 523 } 524 525 /**************************************************************************************************************/ 526 527 /* 528 Build a texture from an image and store in a renderable 529 */ 530 EquatorialGridLayer.prototype._buildTextureFromImage = function(renderable,image) 531 { 532 renderable.texture = this.texturePool.createGLTexture(image); 533 renderable.textureWidth = image.width; 534 renderable.textureHeight = image.height; 535 } 536 537 /**************************************************************************************************************/ 538 539 /** 540 * @constructor 541 * GL Textures pool 542 */ 543 var TexturePool = function(gl) 544 { 545 var gl = gl; 546 var glTextures = []; 547 548 /** 549 Create a GL texture 550 */ 551 this.createGLTexture = function(image) 552 { 553 if ( glTextures.length > 0 ) 554 { 555 return reuseGLTexture(image); 556 } 557 else 558 { 559 return createNewGLTexture(image); 560 } 561 }; 562 563 564 /** 565 Dispose a GL texture 566 */ 567 this.disposeGLTexture = function( texture ) 568 { 569 glTextures.push(texture); 570 } 571 572 this.disposeAll = function() 573 { 574 for ( var i=0; i<glTextures.length; i++ ) 575 { 576 gl.deleteTexture(glTextures[i]); 577 } 578 glTextures.length = 0; 579 } 580 581 /** 582 Create a non power of two texture from an image 583 */ 584 var createNewGLTexture = function(image) 585 { 586 var tex = gl.createTexture(); 587 gl.bindTexture(gl.TEXTURE_2D, tex); 588 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 589 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 590 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 591 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 592 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 593 return tex; 594 } 595 596 597 /** 598 Reuse a GL texture 599 */ 600 var reuseGLTexture = function(image) 601 { 602 var glTexture = glTextures.pop(); 603 gl.bindTexture(gl.TEXTURE_2D, glTexture); 604 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 605 return glTexture; 606 } 607 } 608 609 /**************************************************************************************************************/ 610 611 return EquatorialGridLayer; 612 613 });