(function() {var Color = (function() { var w3cColors = { aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4', azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000', blanchedalmond: '#ffebcd', blue: '#0000ff', blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c', cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9', darkgrey: '#a9a9a9', darkgreen: '#006400', darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969', dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22', fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', gray: '#808080', grey: '#808080', green: '#008000', greenyellow: '#adff2f', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c', indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa', lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgrey: '#d3d3d3', lightgreen: '#90ee90', lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32', linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa', mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371', mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5', navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000', olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb', plum: '#dda0dd', powderblue: '#b0e0e6', purple: '#800080', rebeccapurple: '#663399', red: '#ff0000', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee', sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb', slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', teal: '#008080', thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3', white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00', yellowgreen: '#9acd32' }; function hue2rgb(p, q, t) { if (t<0) t += 1; if (t>1) t -= 1; if (t<1/6) return p + (q - p)*6*t; if (t<1/2) return q; if (t<2/3) return p + (q - p)*(2/3 - t)*6; return p; } function clamp(v, max) { return Math.min(max, Math.max(0, v || 0)); } /** * @param str, object can be in any of these: 'red', '#0099ff', 'rgb(64, 128, 255)', 'rgba(64, 128, 255, 0.5)', { r:0.2, g:0.3, b:0.9, a:1 } */ var Color = function(r, g, b, a) { this.r = clamp(r, 1); this.g = clamp(g, 1); this.b = clamp(b, 1); this.a = (a !== undefined ? clamp(a, 1) : 1); }; /** * @param str, object can be in any of these: 'red', '#0099ff', 'rgb(64, 128, 255)', 'rgba(64, 128, 255, 0.5)' */ Color.parse = function(str) { if (typeof str === 'string') { str = str.toLowerCase(); str = w3cColors[str] || str; var m; if ((m = str.match(/^#?(\w{2})(\w{2})(\w{2})$/))) { return new Color(parseInt(m[1], 16)/255, parseInt(m[2], 16)/255, parseInt(m[3], 16)/255); } if ((m = str.match(/rgba?\((\d+)\D+(\d+)\D+(\d+)(\D+([\d.]+))?\)/))) { return new Color( parseFloat(m[1])/255, parseFloat(m[2])/255, parseFloat(m[3])/255, m[4] ? parseFloat(m[5]) : 1 ); } } }; Color.fromHSL = function(h, s, l, a) { // h = clamp(h, 360), // s = clamp(s, 1), // l = clamp(l, 1), // achromatic if (s === 0) { return new Color(l, l, l, a); } var q = l<0.5 ? l*(1 + s) : l + s - l*s, p = 2*l - q; h /= 360; return new Color( hue2rgb(p, q, h + 1/3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1/3), a ); }; Color.prototype = { toHSL: function() { if (this.r === undefined || this.g === undefined || this.b === undefined) { return; } var max = Math.max(this.r, this.g, this.b), min = Math.min(this.r, this.g, this.b), h, s, l = (max + min)/2, d = max - min; if (!d) { h = s = 0; // achromatic } else { s = l>0.5 ? d/(2 - max - min) : d/(max + min); switch (max) { case this.r: h = (this.g - this.b)/d + (this.g uFogRadius) {\n vColor = vec4(0.0, 0.0, 0.0, 0.0);\n } else {\n vColor = vec4(aId, 1.0);\n }\n }\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nvarying vec4 vColor;\nvoid main() {\n gl_FragColor = vColor;\n}\n"},"buildings":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\n#define halfPi 1.57079632679\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nattribute vec3 aNormal;\nattribute vec3 aColor;\nattribute vec3 aId;\nattribute vec4 aFilter;\nattribute float aHeight;\nuniform mat4 uModelMatrix;\nuniform mat4 uMatrix;\nuniform mat3 uNormalTransform;\nuniform vec3 uLightDirection;\nuniform vec3 uLightColor;\nuniform vec3 uHighlightColor;\nuniform vec3 uHighlightId;\nuniform vec2 uViewDirOnMap;\nuniform vec2 uLowerEdgePoint;\nuniform float uTime;\nvarying vec3 vColor;\nvarying vec2 vTexCoord;\nvarying float verticalDistanceToLowerEdge;\nconst float gradientStrength = 0.4;\nvoid main() {\n float t = clamp((uTime-aFilter.r) / (aFilter.g-aFilter.r), 0.0, 1.0);\n float f = aFilter.b + (aFilter.a-aFilter.b) * t;\n if (f == 0.0) {\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n vColor = vec3(0.0, 0.0, 0.0);\n } else {\n vec4 pos = vec4(aPosition.x, aPosition.y, aPosition.z*f, aPosition.w);\n gl_Position = uMatrix * pos;\n //*** highlight object ******************************************************\n vec3 color = aColor;\n if (uHighlightId == aId) {\n color = mix(aColor, uHighlightColor, 0.5);\n }\n //*** light intensity, defined by light direction on surface ****************\n vec3 transformedNormal = aNormal * uNormalTransform;\n float lightIntensity = max( dot(transformedNormal, uLightDirection), 0.0) / 1.5;\n color = color + uLightColor * lightIntensity;\n vTexCoord = aTexCoord;\n //*** vertical shading ******************************************************\n float verticalShading = clamp(gradientStrength - ((pos.z*gradientStrength) / aHeight), 0.0, gradientStrength);\n //***************************************************************************\n vColor = color-verticalShading;\n vec4 worldPos = uModelMatrix * pos;\n vec2 dirFromLowerEdge = worldPos.xy / worldPos.w - uLowerEdgePoint;\n verticalDistanceToLowerEdge = dot(dirFromLowerEdge, uViewDirOnMap);\n }\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nvarying vec3 vColor;\nvarying vec2 vTexCoord;\nvarying float verticalDistanceToLowerEdge;\nuniform vec3 uFogColor;\nuniform float uFogDistance;\nuniform float uFogBlurDistance;\nuniform sampler2D uWallTexIndex;\nvoid main() {\n \n float fogIntensity = (verticalDistanceToLowerEdge - uFogDistance) / uFogBlurDistance;\n fogIntensity = clamp(fogIntensity, 0.0, 1.0);\n gl_FragColor = vec4( vColor* texture2D(uWallTexIndex, vTexCoord).rgb, 1.0-fogIntensity);\n}\n"},"buildings.shadows":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\n#define halfPi 1.57079632679\nattribute vec4 aPosition;\nattribute vec3 aNormal;\nattribute vec3 aColor;\nattribute vec2 aTexCoord;\nattribute vec3 aId;\nattribute vec4 aFilter;\nattribute float aHeight;\nuniform mat4 uModelMatrix;\nuniform mat4 uMatrix;\nuniform mat4 uSunMatrix;\nuniform mat3 uNormalTransform;\nuniform vec3 uHighlightColor;\nuniform vec3 uHighlightId;\nuniform vec2 uViewDirOnMap;\nuniform vec2 uLowerEdgePoint;\nuniform float uTime;\nvarying vec3 vColor;\nvarying vec2 vTexCoord;\nvarying vec3 vNormal;\nvarying vec3 vSunRelPosition;\nvarying float verticalDistanceToLowerEdge;\nfloat gradientStrength = 0.4;\nvoid main() {\n float t = clamp((uTime-aFilter.r) / (aFilter.g-aFilter.r), 0.0, 1.0);\n float f = aFilter.b + (aFilter.a-aFilter.b) * t;\n if (f == 0.0) {\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n vColor = vec3(0.0, 0.0, 0.0);\n } else {\n vec4 pos = vec4(aPosition.x, aPosition.y, aPosition.z*f, aPosition.w);\n gl_Position = uMatrix * pos;\n //*** highlight object ******************************************************\n vec3 color = aColor;\n if (uHighlightId == aId) {\n color = mix(aColor, uHighlightColor, 0.5);\n }\n //*** light intensity, defined by light direction on surface ****************\n vNormal = aNormal;\n vTexCoord = aTexCoord;\n //vec3 transformedNormal = aNormal * uNormalTransform;\n //float lightIntensity = max( dot(aNormal, uLightDirection), 0.0) / 1.5;\n //color = color + uLightColor * lightIntensity;\n //*** vertical shading ******************************************************\n float verticalShading = clamp(gradientStrength - ((pos.z*gradientStrength) / aHeight), 0.0, gradientStrength);\n //***************************************************************************\n vColor = color-verticalShading;\n vec4 worldPos = uModelMatrix * pos;\n vec2 dirFromLowerEdge = worldPos.xy / worldPos.w - uLowerEdgePoint;\n verticalDistanceToLowerEdge = dot(dirFromLowerEdge, uViewDirOnMap);\n \n // *** shadow mapping ********\n vec4 sunRelPosition = uSunMatrix * pos;\n vSunRelPosition = (sunRelPosition.xyz / sunRelPosition.w + 1.0) / 2.0;\n }\n}\n","fragment":"\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n precision highp float;\n#else\n precision mediump float;\n#endif\nvarying vec2 vTexCoord;\nvarying vec3 vColor;\nvarying vec3 vNormal;\nvarying vec3 vSunRelPosition;\nvarying float verticalDistanceToLowerEdge;\nuniform vec3 uFogColor;\nuniform vec2 uShadowTexDimensions;\nuniform sampler2D uShadowTexIndex;\nuniform sampler2D uWallTexIndex;\nuniform float uFogDistance;\nuniform float uFogBlurDistance;\nuniform float uShadowStrength;\nuniform vec3 uLightDirection;\nuniform vec3 uLightColor;\nfloat isSeenBySun(const vec2 sunViewNDC, const float depth, const float bias) {\n if ( clamp( sunViewNDC, 0.0, 1.0) != sunViewNDC) //not inside sun's viewport\n return 1.0;\n \n float depthFromTexture = texture2D( uShadowTexIndex, sunViewNDC.xy).x;\n \n //compare depth values not in reciprocal but in linear depth\n return step(1.0/depthFromTexture, 1.0/depth + bias);\n}\nvoid main() {\n vec3 normal = normalize(vNormal); //may degenerate during per-pixel interpolation\n float diffuse = dot(uLightDirection, normal);\n diffuse = max(diffuse, 0.0);\n // reduce shadow strength with:\n // - lowering sun positions, to be consistent with the shadows on the basemap (there,\n // shadows are faded out with lowering sun positions to hide shadow artifacts caused\n // when sun direction and map surface are almost perpendicular\n // - large angles between the sun direction and the surface normal, to hide shadow\n // artifacts that occur when surface normal and sun direction are almost perpendicular\n float shadowStrength = pow( max( min(\n dot(uLightDirection, vec3(0.0, 0.0, 1.0)),\n dot(uLightDirection, normal)\n ), 0.0), 1.5);\n if (diffuse > 0.0 && shadowStrength > 0.0) {\n // note: the diffuse term is also the cosine between the surface normal and the\n // light direction\n float bias = clamp(0.0007*tan(acos(diffuse)), 0.0, 0.01);\n vec2 pos = fract( vSunRelPosition.xy * uShadowTexDimensions);\n \n vec2 tl = floor(vSunRelPosition.xy * uShadowTexDimensions) / uShadowTexDimensions;\n float tlVal = isSeenBySun( tl, vSunRelPosition.z, bias);\n float trVal = isSeenBySun( tl + vec2(1.0, 0.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n float blVal = isSeenBySun( tl + vec2(0.0, 1.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n float brVal = isSeenBySun( tl + vec2(1.0, 1.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n float occludedBySun = mix( \n mix(tlVal, trVal, pos.x), \n mix(blVal, brVal, pos.x),\n pos.y);\n diffuse *= 1.0 - (shadowStrength * (1.0 - occludedBySun));\n }\n vec3 color = vColor* texture2D( uWallTexIndex, vTexCoord.st).rgb +\n (diffuse/1.5) * uLightColor;\n float fogIntensity = (verticalDistanceToLowerEdge - uFogDistance) / uFogBlurDistance;\n fogIntensity = clamp(fogIntensity, 0.0, 1.0);\n //gl_FragColor = vec4( mix(color, uFogColor, fogIntensity), 1.0);\n gl_FragColor = vec4( color, 1.0-fogIntensity);\n}\n"},"flatColor":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nuniform mat4 uMatrix;\nvoid main() {\n gl_Position = uMatrix * aPosition;\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nuniform vec4 uColor;\nvoid main() {\n gl_FragColor = uColor;\n}\n"},"skywall":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\n#define halfPi 1.57079632679\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nuniform mat4 uMatrix;\nuniform float uAbsoluteHeight;\nvarying vec2 vTexCoord;\nvarying float vRelativeHeight;\nconst float gradientHeight = 10.0;\nconst float gradientStrength = 1.0;\nvoid main() {\n gl_Position = uMatrix * aPosition;\n vTexCoord = aTexCoord;\n vRelativeHeight = aPosition.z / uAbsoluteHeight;\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nuniform sampler2D uTexIndex;\nuniform vec3 uFogColor;\nvarying vec2 vTexCoord;\nvarying float vRelativeHeight;\nvoid main() {\n float blendFactor = min(100.0 * vRelativeHeight, 1.0);\n vec4 texColor = texture2D(uTexIndex, vTexCoord);\n gl_FragColor = mix( vec4(uFogColor, 1.0), texColor, blendFactor);\n}\n"},"basemap":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\n#define halfPi 1.57079632679\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nuniform mat4 uModelMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uProjMatrix;\nuniform mat4 uMatrix;\nuniform vec2 uViewDirOnMap;\nuniform vec2 uLowerEdgePoint;\nvarying vec2 vTexCoord;\nvarying float verticalDistanceToLowerEdge;\nvoid main() {\n gl_Position = uMatrix * aPosition;\n vTexCoord = aTexCoord;\n vec4 worldPos = uModelMatrix * aPosition;\n vec2 dirFromLowerEdge = worldPos.xy / worldPos.w - uLowerEdgePoint;\n verticalDistanceToLowerEdge = dot(dirFromLowerEdge, uViewDirOnMap);\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nuniform sampler2D uTexIndex;\nuniform vec3 uFogColor;\nvarying vec2 vTexCoord;\nvarying float verticalDistanceToLowerEdge;\nuniform float uFogDistance;\nuniform float uFogBlurDistance;\nvoid main() {\n float fogIntensity = (verticalDistanceToLowerEdge - uFogDistance) / uFogBlurDistance;\n fogIntensity = clamp(fogIntensity, 0.0, 1.0);\n gl_FragColor = vec4(texture2D(uTexIndex, vec2(vTexCoord.x, 1.0-vTexCoord.y)).rgb, 1.0-fogIntensity);\n}\n"},"texture":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nuniform mat4 uMatrix;\nvarying vec2 vTexCoord;\nvoid main() {\n gl_Position = uMatrix * aPosition;\n vTexCoord = aTexCoord;\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nuniform sampler2D uTexIndex;\nvarying vec2 vTexCoord;\nvoid main() {\n gl_FragColor = vec4(texture2D(uTexIndex, vTexCoord.st).rgb, 1.0);\n}\n"},"fogNormal":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nattribute vec4 aFilter;\nattribute vec3 aNormal;\nuniform mat4 uMatrix;\nuniform mat4 uModelMatrix;\nuniform mat3 uNormalMatrix;\nuniform vec2 uViewDirOnMap;\nuniform vec2 uLowerEdgePoint;\nvarying float verticalDistanceToLowerEdge;\nvarying vec3 vNormal;\nuniform float uTime;\nvoid main() {\n float t = clamp((uTime-aFilter.r) / (aFilter.g-aFilter.r), 0.0, 1.0);\n float f = aFilter.b + (aFilter.a-aFilter.b) * t;\n if (f == 0.0) {\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n verticalDistanceToLowerEdge = 0.0;\n } else {\n vec4 pos = vec4(aPosition.x, aPosition.y, aPosition.z*f, aPosition.w);\n gl_Position = uMatrix * pos;\n vNormal = uNormalMatrix * aNormal;\n vec4 worldPos = uModelMatrix * pos;\n vec2 dirFromLowerEdge = worldPos.xy / worldPos.w - uLowerEdgePoint;\n verticalDistanceToLowerEdge = dot(dirFromLowerEdge, uViewDirOnMap);\n }\n}\n","fragment":"\n#ifdef GL_ES\n precision mediump float;\n#endif\nuniform float uFogDistance;\nuniform float uFogBlurDistance;\nvarying float verticalDistanceToLowerEdge;\nvarying vec3 vNormal;\nvoid main() {\n float fogIntensity = (verticalDistanceToLowerEdge - uFogDistance) / uFogBlurDistance;\n gl_FragColor = vec4(normalize(vNormal) / 2.0 + 0.5, clamp(fogIntensity, 0.0, 1.0));\n}\n"},"ambientFromDepth":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nvarying vec2 vTexCoord;\nvoid main() {\n gl_Position = aPosition;\n vTexCoord = aTexCoord;\n}\n","fragment":"#ifdef GL_FRAGMENT_PRECISION_HIGH\n // we need high precision for the depth values\n precision highp float;\n#else\n precision mediump float;\n#endif\nuniform sampler2D uDepthTexIndex;\nuniform sampler2D uFogTexIndex;\nuniform vec2 uInverseTexSize; //in 1/pixels, e.g. 1/512 if the texture is 512px wide\nuniform float uEffectStrength;\nuniform float uNearPlane;\nuniform float uFarPlane;\nvarying vec2 vTexCoord;\n/* Retrieves the depth value 'offset' pixels away from 'pos' from texture 'uDepthTexIndex'. */\nfloat getDepth(vec2 pos, ivec2 offset)\n{\n float z = texture2D(uDepthTexIndex, pos + float(offset) * uInverseTexSize).x;\n return (2.0 * uNearPlane) / (uFarPlane + uNearPlane - z * (uFarPlane - uNearPlane)); // linearize depth\n}\n/* getOcclusionFactor() determines a heuristic factor (from [0..1]) for how \n * much the fragment at 'pos' with depth 'depthHere'is occluded by the \n * fragment that is (dx, dy) texels away from it.\n */\nfloat getOcclusionFactor(float depthHere, vec2 pos, ivec2 offset)\n{\n float depthThere = getDepth(pos, offset);\n /* if the fragment at (dx, dy) has no depth (i.e. there was nothing rendered there), \n * then 'here' is not occluded (result 1.0) */\n if (depthThere == 0.0)\n return 1.0;\n /* if the fragment at (dx, dy) is further away from the viewer than 'here', then\n * 'here is not occluded' */\n if (depthHere < depthThere )\n return 1.0;\n \n float relDepthDiff = depthThere / depthHere;\n float depthDiff = abs(depthThere - depthHere) * uFarPlane;\n /* if the fragment at (dx, dy) is closer to the viewer than 'here', then it occludes\n * 'here'. The occlusion is the higher the bigger the depth difference between the two\n * locations is.\n * However, if the depth difference is too high, we assume that 'there' lies in a\n * completely different depth region of the scene than 'here' and thus cannot occlude\n * 'here'. This last assumption gets rid of very dark artifacts around tall buildings.\n */\n return depthDiff < 50.0 ? mix(0.99, 1.0, 1.0 - clamp(depthDiff, 0.0, 1.0)) : 1.0;\n}\n/* This shader approximates the ambient occlusion in screen space (SSAO). \n * It is based on the assumption that a pixel will be occluded by neighboring \n * pixels iff. those have a depth value closer to the camera than the original\n * pixel itself (the function getOcclusionFactor() computes this occlusion \n * by a single other pixel).\n *\n * A naive approach would sample all pixels within a given distance. For an\n * interesting-looking effect, the sampling area needs to be at least 9 pixels \n * wide (-/+ 4), requiring 81 texture lookups per pixel for ambient occlusion.\n * This overburdens many GPUs.\n * To make the ambient occlusion computation faster, we do not consider all \n * texels in the sampling area, but only 16. This causes some sampling artifacts\n * that are later removed by blurring the ambient occlusion texture (this is \n * done in a separate shader).\n */\nvoid main() {\n float depthHere = getDepth(vTexCoord, ivec2(0, 0));\n float fogIntensity = texture2D(uFogTexIndex, vTexCoord).w;\n if (depthHere == 0.0)\n {\n\t//there was nothing rendered 'here' --> it can't be occluded\n gl_FragColor = vec4(1.0);\n return;\n }\n float occlusionFactor = 1.0;\n \n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-1, 0));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+1, 0));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2( 0, -1));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2( 0, +1));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-2, -2));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+2, +2));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+2, -2));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-2, +2));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-4, 0));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+4, 0));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2( 0, -4));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2( 0, +4));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-4, -4));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+4, +4));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(+4, -4));\n occlusionFactor *= getOcclusionFactor(depthHere, vTexCoord, ivec2(-4, +4));\n occlusionFactor = pow(occlusionFactor, 4.0) + 55.0/255.0; // empirical bias determined to let SSAO have no effect on the map plane\n occlusionFactor = 1.0 - ((1.0 - occlusionFactor) * uEffectStrength * (1.0-fogIntensity));\n gl_FragColor = vec4(vec3(occlusionFactor), 1.0);\n}\n"},"blur":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nvarying vec2 vTexCoord;\nvoid main() {\n gl_Position = aPosition;\n vTexCoord = aTexCoord;\n}\n","fragment":"#ifdef GL_ES\n precision mediump float;\n#endif\nuniform sampler2D uTexIndex;\nuniform vec2 uInverseTexSize; //in 1/pixels, e.g. 1/512 if the texture is 512px wide\nvarying vec2 vTexCoord;\n/* Retrieves the texel color 'offset' pixels away from 'pos' from texture 'uTexIndex'. */\nvec4 getTexel(vec2 pos, vec2 offset)\n{\n return texture2D(uTexIndex, pos + offset * uInverseTexSize);\n}\nvoid main() {\n vec4 center = texture2D(uTexIndex, vTexCoord);\n vec4 nonDiagonalNeighbors = getTexel(vTexCoord, vec2(-1.0, 0.0)) +\n getTexel(vTexCoord, vec2(+1.0, 0.0)) +\n getTexel(vTexCoord, vec2( 0.0, -1.0)) +\n getTexel(vTexCoord, vec2( 0.0, +1.0));\n vec4 diagonalNeighbors = getTexel(vTexCoord, vec2(-1.0, -1.0)) +\n getTexel(vTexCoord, vec2(+1.0, +1.0)) +\n getTexel(vTexCoord, vec2(-1.0, +1.0)) +\n getTexel(vTexCoord, vec2(+1.0, -1.0));\n \n //approximate Gaussian blur (mean 0.0, stdev 1.0)\n gl_FragColor = 0.2/1.0 * center + \n 0.5/4.0 * nonDiagonalNeighbors + \n 0.3/4.0 * diagonalNeighbors;\n}\n"},"basemap.shadows":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec3 aPosition;\nattribute vec3 aNormal;\nuniform mat4 uModelMatrix;\nuniform mat4 uMatrix;\nuniform mat4 uSunMatrix;\nuniform vec2 uViewDirOnMap;\nuniform vec2 uLowerEdgePoint;\n//varying vec2 vTexCoord;\nvarying vec3 vSunRelPosition;\nvarying vec3 vNormal;\nvarying float verticalDistanceToLowerEdge;\nvoid main() {\n vec4 pos = vec4(aPosition.xyz, 1.0);\n gl_Position = uMatrix * pos;\n vec4 sunRelPosition = uSunMatrix * pos;\n vSunRelPosition = (sunRelPosition.xyz / sunRelPosition.w + 1.0) / 2.0;\n vNormal = aNormal;\n vec4 worldPos = uModelMatrix * pos;\n vec2 dirFromLowerEdge = worldPos.xy / worldPos.w - uLowerEdgePoint;\n verticalDistanceToLowerEdge = dot(dirFromLowerEdge, uViewDirOnMap);\n}\n","fragment":"\n#ifdef GL_FRAGMENT_PRECISION_HIGH\n precision highp float;\n#else\n precision mediump float;\n#endif\n/* This shader computes the diffuse brightness of the map layer. It does *not* \n * render the map texture itself, but is instead intended to be blended on top\n * of an already rendered map.\n * Note: this shader is not (and does not attempt to) be physically correct.\n * It is intented to be a blend between a useful illustration of cast\n * shadows and a mitigation of shadow casting artifacts occuring at\n * low angles on incidence.\n * Map brightness is only affected by shadows, not by light direction.\n * Shadows are darkest when light comes from straight above (and thus\n * shadows can be computed reliably) and become less and less visible\n * with the light source close to the horizont (where moirC) and offset\n * artifacts would otherwise be visible).\n */\n//uniform sampler2D uTexIndex;\nuniform sampler2D uShadowTexIndex;\nuniform vec3 uFogColor;\nuniform vec3 uDirToSun;\nuniform vec2 uShadowTexDimensions;\nuniform float uShadowStrength;\nvarying vec2 vTexCoord;\nvarying vec3 vSunRelPosition;\nvarying vec3 vNormal;\nvarying float verticalDistanceToLowerEdge;\nuniform float uFogDistance;\nuniform float uFogBlurDistance;\nfloat isSeenBySun( const vec2 sunViewNDC, const float depth, const float bias) {\n if ( clamp( sunViewNDC, 0.0, 1.0) != sunViewNDC) //not inside sun's viewport\n return 1.0;\n \n float depthFromTexture = texture2D( uShadowTexIndex, sunViewNDC.xy).x;\n \n //compare depth values not in reciprocal but in linear depth\n return step(1.0/depthFromTexture, 1.0/depth + bias);\n}\nvoid main() {\n //vec2 tl = floor(vSunRelPosition.xy * uShadowTexDimensions) / uShadowTexDimensions;\n //gl_FragColor = vec4(vec3(texture2D( uShadowTexIndex, tl).x), 1.0);\n //return;\n float diffuse = dot(uDirToSun, normalize(vNormal));\n diffuse = max(diffuse, 0.0);\n \n float shadowStrength = uShadowStrength * pow(diffuse, 1.5);\n if (diffuse > 0.0) {\n // note: the diffuse term is also the cosine between the surface normal and the\n // light direction\n float bias = clamp(0.0007*tan(acos(diffuse)), 0.0, 0.01);\n \n vec2 pos = fract( vSunRelPosition.xy * uShadowTexDimensions);\n \n vec2 tl = floor(vSunRelPosition.xy * uShadowTexDimensions) / uShadowTexDimensions;\n float tlVal = isSeenBySun( tl, vSunRelPosition.z, bias);\n float trVal = isSeenBySun( tl + vec2(1.0, 0.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n float blVal = isSeenBySun( tl + vec2(0.0, 1.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n float brVal = isSeenBySun( tl + vec2(1.0, 1.0) / uShadowTexDimensions, vSunRelPosition.z, bias);\n diffuse = mix( mix(tlVal, trVal, pos.x), \n mix(blVal, brVal, pos.x),\n pos.y);\n }\n diffuse = mix(1.0, diffuse, shadowStrength);\n \n float fogIntensity = (verticalDistanceToLowerEdge - uFogDistance) / uFogBlurDistance;\n fogIntensity = clamp(fogIntensity, 0.0, 1.0);\n float darkness = (1.0 - diffuse);\n darkness *= (1.0 - fogIntensity);\n gl_FragColor = vec4(vec3(1.0 - darkness), 1.0);\n}\n"},"outlineMap":{"vertex":"precision highp float; //is default in vertex shaders anyway, using highp fixes #49\nattribute vec4 aPosition;\nattribute vec2 aTexCoord;\nuniform mat4 uMatrix;\nvarying vec2 vTexCoord;\nvoid main() {\n gl_Position = uMatrix * aPosition;\n vTexCoord = aTexCoord;\n}\n","fragment":"#ifdef GL_FRAGMENT_PRECISION_HIGH\n // we need high precision for the depth values\n precision highp float;\n#else\n precision mediump float;\n#endif\nuniform sampler2D uDepthTexIndex;\nuniform sampler2D uFogNormalTexIndex;\nuniform sampler2D uIdTexIndex;\nuniform vec2 uInverseTexSize; //in 1/pixels, e.g. 1/512 if the texture is 512px wide\nuniform float uEffectStrength;\nuniform float uNearPlane;\nuniform float uFarPlane;\nvarying vec2 vTexCoord;\n/* Retrieves the depth value 'offset' pixels away from 'pos' from texture 'uDepthTexIndex'. */\nfloat getDepth(vec2 pos, vec2 offset)\n{\n float z = texture2D(uDepthTexIndex, pos + offset * uInverseTexSize).x;\n return (2.0 * uNearPlane) / (uFarPlane + uNearPlane - z * (uFarPlane - uNearPlane)); // linearize depth\n}\nvec3 getNormal(vec2 pos, vec2 offset)\n{\n return normalize(texture2D(uFogNormalTexIndex, pos + offset * uInverseTexSize).xyz * 2.0 - 1.0);\n}\nvec3 getEncodedId(vec2 pos, vec2 offset)\n{\n return texture2D(uIdTexIndex, pos + offset * uInverseTexSize).xyz;\n}\nvoid main() {\n float fogIntensity = texture2D(uFogNormalTexIndex, vTexCoord).w;\n vec3 normalHere = getNormal(vTexCoord, vec2(0, 0));\n vec3 normalRight = getNormal(vTexCoord, vec2(1, 0));\n vec3 normalAbove = getNormal(vTexCoord, vec2(0,-1));\n \n float edgeStrengthFromNormal = \n step( dot(normalHere, normalRight), 0.9) +\n step( dot(normalHere, normalAbove), 0.9);\n float depthHere = getDepth(vTexCoord, vec2(0, 0));\n float depthRight = getDepth(vTexCoord, vec2(1, 0));\n float depthAbove = getDepth(vTexCoord, vec2(0, -1));\n float depthDiffRight = abs(depthHere - depthRight) * 7500.0;\n float depthDiffAbove = abs(depthHere - depthAbove) * 7500.0;\n float edgeStrengthFromDepth = step(10.0, depthDiffRight) + \n step(10.0, depthDiffAbove);\n \n vec3 idHere = getEncodedId(vTexCoord, vec2(0,0));\n vec3 idRight = getEncodedId(vTexCoord, vec2(1,0));\n vec3 idAbove = getEncodedId(vTexCoord, vec2(0,-1));\n float edgeStrengthFromId = (idHere != idRight || idHere != idAbove) ? 1.0 : 0.0;\n \n float edgeStrength = max( edgeStrengthFromId, max( edgeStrengthFromNormal, edgeStrengthFromDepth));\n float occlusionFactor = 1.0 - (edgeStrength * uEffectStrength);\n occlusionFactor = 1.0 - ((1.0- occlusionFactor) * (1.0-fogIntensity));\n gl_FragColor = vec4(vec3(occlusionFactor), 1.0);\n}\n"}}; var GLX = (function() { //var ext = GL.getExtension('WEBGL_lose_context'); //ext.loseContext(); var GLX = {}; GLX.getContext = function(canvas) { var options = { antialias: !APP.options.fastMode, depth: true, premultipliedAlpha: false }; try { GL = canvas.getContext('webgl', options); } catch (ex) {} if (!GL) { try { GL = canvas.getContext('experimental-webgl', options); } catch (ex) {} } if (!GL) { throw new Error('WebGL not supported'); } canvas.addEventListener('webglcontextlost', function(e) { console.warn('context lost'); }); canvas.addEventListener('webglcontextrestored', function(e) { console.warn('context restored'); }); GL.viewport(0, 0, APP.width, APP.height); GL.cullFace(GL.BACK); GL.enable(GL.CULL_FACE); GL.enable(GL.DEPTH_TEST); GL.clearColor(0.5, 0.5, 0.5, 1); if (!APP.options.fastMode) { GL.anisotropyExtension = GL.getExtension('EXT_texture_filter_anisotropic'); if (GL.anisotropyExtension) { GL.anisotropyExtension.maxAnisotropyLevel = GL.getParameter( GL.anisotropyExtension.MAX_TEXTURE_MAX_ANISOTROPY_EXT ); } GL.depthTextureExtension = GL.getExtension('WEBGL_depth_texture'); } return GL; }; GLX.start = function(render) { return setInterval(function() { requestAnimationFrame(render); }, 17); }; GLX.stop = function(loop) { clearInterval(loop); }; GLX.destroy = function() { if (GL !== undefined) { GL.canvas.parentNode.removeChild(GL.canvas); GL = undefined; } }; GLX.util = {}; GLX.util.nextPowerOf2 = function(n) { n--; n |= n >> 1; // handle 2 bit numbers n |= n >> 2; // handle 4 bit numbers n |= n >> 4; // handle 8 bit numbers n |= n >> 8; // handle 16 bit numbers n |= n >> 16; // handle 32 bit numbers n++; return n; }; GLX.util.calcNormal = function(ax, ay, az, bx, by, bz, cx, cy, cz) { var d1x = ax-bx; var d1y = ay-by; var d1z = az-bz; var d2x = bx-cx; var d2y = by-cy; var d2z = bz-cz; var nx = d1y*d2z - d1z*d2y; var ny = d1z*d2x - d1x*d2z; var nz = d1x*d2y - d1y*d2x; return this.calcUnit(nx, ny, nz); }; GLX.util.calcUnit = function(x, y, z) { var m = Math.sqrt(x*x + y*y + z*z); if (m === 0) { m = 0.00001; } return [x/m, y/m, z/m]; }; GLX.Buffer = function(itemSize, data) { this.id = GL.createBuffer(); this.itemSize = itemSize; this.numItems = data.length/itemSize; GL.bindBuffer(GL.ARRAY_BUFFER, this.id); GL.bufferData(GL.ARRAY_BUFFER, data, GL.STATIC_DRAW); data = null; }; GLX.Buffer.prototype = { enable: function() { GL.bindBuffer(GL.ARRAY_BUFFER, this.id); }, destroy: function() { GL.deleteBuffer(this.id); this.id = null; } }; GLX.Framebuffer = function(width, height, useDepthTexture) { if (useDepthTexture && !GL.depthTextureExtension) throw "Depth textures are not supported by your GPU"; this.useDepthTexture = !!useDepthTexture; this.setSize(width, height); }; GLX.Framebuffer.prototype = { setSize: function(width, height) { if (!this.frameBuffer) { this.frameBuffer = GL.createFramebuffer(); } else if (width === this.width && height === this.height) { // already has the right size return; } GL.bindFramebuffer(GL.FRAMEBUFFER, this.frameBuffer); this.width = width; this.height = height; if (this.depthRenderBuffer) { GL.deleteRenderbuffer(this.depthRenderBuffer); this.depthRenderBuffer = null; } if (this.depthTexture) { this.depthTexture.destroy(); this.depthTexture = null; } if (this.useDepthTexture) { this.depthTexture = new GLX.texture.Image();//GL.createTexture(); this.depthTexture.enable(0); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.NEAREST); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.NEAREST); //CLAMP_TO_EDGE is required for NPOT textures GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.texImage2D(GL.TEXTURE_2D, 0, GL.DEPTH_STENCIL, width, height, 0, GL.DEPTH_STENCIL, GL.depthTextureExtension.UNSIGNED_INT_24_8_WEBGL, null); GL.framebufferTexture2D(GL.FRAMEBUFFER, GL.DEPTH_STENCIL_ATTACHMENT, GL.TEXTURE_2D, this.depthTexture.id, 0); } else { this.depthRenderBuffer = GL.createRenderbuffer(); GL.bindRenderbuffer(GL.RENDERBUFFER, this.depthRenderBuffer); GL.renderbufferStorage(GL.RENDERBUFFER, GL.DEPTH_COMPONENT16, width, height); GL.framebufferRenderbuffer(GL.FRAMEBUFFER, GL.DEPTH_ATTACHMENT, GL.RENDERBUFFER, this.depthRenderBuffer); } if (this.renderTexture) { this.renderTexture.destroy(); } this.renderTexture = new GLX.texture.Data(width, height); GL.bindTexture(GL.TEXTURE_2D, this.renderTexture.id); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); //necessary for NPOT textures GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.framebufferTexture2D(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, GL.TEXTURE_2D, this.renderTexture.id, 0); if (GL.checkFramebufferStatus(GL.FRAMEBUFFER) !== GL.FRAMEBUFFER_COMPLETE) { throw new Error('This combination of framebuffer attachments does not work'); } GL.bindRenderbuffer(GL.RENDERBUFFER, null); GL.bindFramebuffer(GL.FRAMEBUFFER, null); }, enable: function() { GL.bindFramebuffer(GL.FRAMEBUFFER, this.frameBuffer); if (!this.useDepthTexture) { GL.bindRenderbuffer(GL.RENDERBUFFER, this.depthRenderBuffer); } }, disable: function() { GL.bindFramebuffer(GL.FRAMEBUFFER, null); if (!this.useDepthTexture) { GL.bindRenderbuffer(GL.RENDERBUFFER, null); } }, getPixel: function(x, y) { var imageData = new Uint8Array(4); if (x < 0 || y < 0 || x >= this.width || y >= this.height) { return; } GL.readPixels(x, y, 1, 1, GL.RGBA, GL.UNSIGNED_BYTE, imageData); return imageData; }, getData: function() { var imageData = new Uint8Array(this.width*this.height*4); GL.readPixels(0, 0, this.width, this.height, GL.RGBA, GL.UNSIGNED_BYTE, imageData); return imageData; }, destroy: function() { if (this.renderTexture) { this.renderTexture.destroy(); } if (this.depthTexture) { this.depthTexture.destroy(); } } }; GLX.Shader = function(config) { var i; this.shaderName = config.shaderName; this.id = GL.createProgram(); this.attach(GL.VERTEX_SHADER, config.vertexShader); this.attach(GL.FRAGMENT_SHADER, config.fragmentShader); GL.linkProgram(this.id); if (!GL.getProgramParameter(this.id, GL.LINK_STATUS)) { throw new Error(GL.getProgramParameter(this.id, GL.VALIDATE_STATUS) +'\n'+ GL.getError()); } this.attributeNames = config.attributes || []; this.uniformNames = config.uniforms || []; GL.useProgram(this.id); this.attributes = {}; for (i = 0; i < this.attributeNames.length; i++) { this.locateAttribute(this.attributeNames[i]); } this.uniforms = {}; for (i = 0; i < this.uniformNames.length; i++) { this.locateUniform(this.uniformNames[i]); } }; GLX.Shader.warned = {}; GLX.Shader.prototype = { locateAttribute: function(name) { var loc = GL.getAttribLocation(this.id, name); if (loc < 0) { console.warn('unable to locate attribute "%s" in shader "%s"', name, this.shaderName); return; } this.attributes[name] = loc; }, locateUniform: function(name) { var loc = GL.getUniformLocation(this.id, name); if (!loc) { console.warn('unable to locate uniform "%s" in shader "%s"', name, this.shaderName); return; } this.uniforms[name] = loc; }, attach: function(type, src) { var shader = GL.createShader(type); GL.shaderSource(shader, src); GL.compileShader(shader); if (!GL.getShaderParameter(shader, GL.COMPILE_STATUS)) { throw new Error(GL.getShaderInfoLog(shader)); } GL.attachShader(this.id, shader); }, enable: function() { GL.useProgram(this.id); for (var name in this.attributes) { GL.enableVertexAttribArray(this.attributes[name]); } return this; }, disable: function() { if (this.attributes) { for (var name in this.attributes) { GL.disableVertexAttribArray(this.attributes[name]); } } }, bindBuffer: function(buffer, attribute) { if (this.attributes[attribute] === undefined) { var qualifiedName = this.shaderName + ":" + attribute; if ( !GLX.Shader.warned[qualifiedName]) { console.warn('attempt to bind VBO to invalid attribute "%s" in shader "%s"', attribute, this.shaderName); GLX.Shader.warned[qualifiedName] = true; } return; } buffer.enable(); GL.vertexAttribPointer(this.attributes[attribute], buffer.itemSize, GL.FLOAT, false, 0, 0); }, setUniform: function(uniform, type, value) { if (this.uniforms[uniform] === undefined) { var qualifiedName = this.shaderName + ":" + uniform; if ( !GLX.Shader.warned[qualifiedName]) { console.warn('attempt to bind to invalid uniform "%s" in shader "%s"', uniform, this.shaderName); GLX.Shader.warned[qualifiedName] = true; } return; } GL['uniform'+ type]( this.uniforms[uniform], value); }, setUniforms: function(uniforms) { for (var i in uniforms) { this.setUniform(uniforms[i][0], uniforms[i][1], uniforms[i][2]); } }, setUniformMatrix: function(uniform, type, value) { if (this.uniforms[uniform] === undefined) { var qualifiedName = this.shaderName + ":" + uniform; if ( !GLX.Shader.warned[qualifiedName]) { console.warn('attempt to bind to invalid uniform "%s" in shader "%s"', uniform, this.shaderName); GLX.Shader.warned[qualifiedName] = true; } return; } GL['uniformMatrix'+ type]( this.uniforms[uniform], false, value); }, setUniformMatrices: function(uniforms) { for (var i in uniforms) { this.setUniformMatrix(uniforms[i][0], uniforms[i][1], uniforms[i][2]); } }, bindTexture: function(uniform, textureUnit, glxTexture) { glxTexture.enable(textureUnit); this.setUniform(uniform, "1i", textureUnit); }, destroy: function() { this.disable(); this.id = null; } }; GLX.Matrix = function(data) { this.data = new Float32Array(data ? data : [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); }; GLX.Matrix.identity = function() { return new GLX.Matrix([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); }; GLX.Matrix.identity3 = function() { return new GLX.Matrix([ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]); }; (function() { function rad(a) { return a * Math.PI/180; } function multiply(res, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = b[0], b01 = b[1], b02 = b[2], b03 = b[3], b10 = b[4], b11 = b[5], b12 = b[6], b13 = b[7], b20 = b[8], b21 = b[9], b22 = b[10], b23 = b[11], b30 = b[12], b31 = b[13], b32 = b[14], b33 = b[15]; res[ 0] = a00*b00 + a01*b10 + a02*b20 + a03*b30; res[ 1] = a00*b01 + a01*b11 + a02*b21 + a03*b31; res[ 2] = a00*b02 + a01*b12 + a02*b22 + a03*b32; res[ 3] = a00*b03 + a01*b13 + a02*b23 + a03*b33; res[ 4] = a10*b00 + a11*b10 + a12*b20 + a13*b30; res[ 5] = a10*b01 + a11*b11 + a12*b21 + a13*b31; res[ 6] = a10*b02 + a11*b12 + a12*b22 + a13*b32; res[ 7] = a10*b03 + a11*b13 + a12*b23 + a13*b33; res[ 8] = a20*b00 + a21*b10 + a22*b20 + a23*b30; res[ 9] = a20*b01 + a21*b11 + a22*b21 + a23*b31; res[10] = a20*b02 + a21*b12 + a22*b22 + a23*b32; res[11] = a20*b03 + a21*b13 + a22*b23 + a23*b33; res[12] = a30*b00 + a31*b10 + a32*b20 + a33*b30; res[13] = a30*b01 + a31*b11 + a32*b21 + a33*b31; res[14] = a30*b02 + a31*b12 + a32*b22 + a33*b32; res[15] = a30*b03 + a31*b13 + a32*b23 + a33*b33; } GLX.Matrix.prototype = { multiply: function(m) { multiply(this.data, this.data, m.data); return this; }, translate: function(x, y, z) { multiply(this.data, this.data, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1 ]); return this; }, rotateX: function(angle) { var a = rad(angle), c = Math.cos(a), s = Math.sin(a); multiply(this.data, this.data, [ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1 ]); return this; }, rotateY: function(angle) { var a = rad(angle), c = Math.cos(a), s = Math.sin(a); multiply(this.data, this.data, [ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1 ]); return this; }, rotateZ: function(angle) { var a = rad(angle), c = Math.cos(a), s = Math.sin(a); multiply(this.data, this.data, [ c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); return this; }, scale: function(x, y, z) { multiply(this.data, this.data, [ x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1 ]); return this; } }; GLX.Matrix.multiply = function(a, b) { var res = new Float32Array(16); multiply(res, a.data, b.data); return res; }; // returns a perspective projection matrix with a field-of-view of 'fov' // degrees, an width/height aspect ratio of 'aspect', the near plane at 'near' // and the far plane at 'far' GLX.Matrix.Perspective = function(fov, aspect, near, far) { var f = 1 / Math.tan(fov*(Math.PI/180)/2), nf = 1 / (near - far); return new GLX.Matrix([ f/aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near)*nf, -1, 0, 0, (2*far*near)*nf, 0]); }; //returns a perspective projection matrix with the near plane at 'near', //the far plane at 'far' and the view rectangle on the near plane bounded //by 'left', 'right', 'top', 'bottom' GLX.Matrix.Frustum = function (left, right, top, bottom, near, far) { var rl = 1 / (right - left), tb = 1 / (top - bottom), nf = 1 / (near - far); return new GLX.Matrix( [ (near * 2) * rl, 0, 0, 0, 0, (near * 2) * tb, 0, 0, (right + left) * rl, (top + bottom) * tb, (far + near) * nf, -1, 0, 0, (far * near * 2) * nf, 0]); }; GLX.Matrix.OffCenterProjection = function (screenBottomLeft, screenTopLeft, screenBottomRight, eye, near, far) { var vRight = norm3(sub3( screenBottomRight, screenBottomLeft)); var vUp = norm3(sub3( screenTopLeft, screenBottomLeft)); var vNormal= normal( screenBottomLeft, screenTopLeft, screenBottomRight); var eyeToScreenBottomLeft = sub3( screenBottomLeft, eye); var eyeToScreenTopLeft = sub3( screenTopLeft, eye); var eyeToScreenBottomRight= sub3( screenBottomRight,eye); var d = - dot3(eyeToScreenBottomLeft, vNormal); var l = dot3(vRight, eyeToScreenBottomLeft) * near / d; var r = dot3(vRight, eyeToScreenBottomRight)* near / d; var b = dot3(vUp, eyeToScreenBottomLeft) * near / d; var t = dot3(vUp, eyeToScreenTopLeft) * near / d; return GLX.Matrix.Frustum(l, r, t, b, near, far); }; // based on http://www.songho.ca/opengl/gl_projectionmatrix.html GLX.Matrix.Ortho = function(left, right, top, bottom, near, far) { return new GLX.Matrix([ 2/(right-left), 0, 0, 0, 0, 2/(top - bottom), 0, 0, 0, 0, -2/(far - near), 0, - (right+left)/(right-left), -(top+bottom)/(top-bottom), - (far+near)/(far-near), 1 ]); }; GLX.Matrix.invert3 = function(a) { var a00 = a[0], a01 = a[1], a02 = a[2], a04 = a[4], a05 = a[5], a06 = a[6], a08 = a[8], a09 = a[9], a10 = a[10], l = a10 * a05 - a06 * a09, o = -a10 * a04 + a06 * a08, m = a09 * a04 - a05 * a08, det = a00*l + a01*o + a02*m; if (!det) { return null; } det = 1.0/det; return [ l * det, (-a10*a01 + a02*a09) * det, ( a06*a01 - a02*a05) * det, o * det, ( a10*a00 - a02*a08) * det, (-a06*a00 + a02*a04) * det, m * det, (-a09*a00 + a01*a08) * det, ( a05*a00 - a01*a04) * det ]; }; GLX.Matrix.transpose3 = function(a) { return new Float32Array([ a[0], a[3], a[6], a[1], a[4], a[7], a[2], a[5], a[8] ]); }; GLX.Matrix.transpose = function(a) { return new Float32Array([ a[0], a[4], a[8], a[12], a[1], a[5], a[9], a[13], a[2], a[6], a[10], a[14], a[3], a[7], a[11], a[15] ]); }; // GLX.Matrix.transform = function(x, y, z, m) { // var X = x*m[0] + y*m[4] + z*m[8] + m[12]; // var Y = x*m[1] + y*m[5] + z*m[9] + m[13]; // var Z = x*m[2] + y*m[6] + z*m[10] + m[14]; // var W = x*m[3] + y*m[7] + z*m[11] + m[15]; // return { // x: (X/W +1) / 2, // y: (Y/W +1) / 2 // }; // }; GLX.Matrix.transform = function(m) { var X = m[12]; var Y = m[13]; var Z = m[14]; var W = m[15]; return { x: (X/W + 1) / 2, y: (Y/W + 1) / 2, z: (Z/W + 1) / 2 }; }; GLX.Matrix.invert = function(a) { var res = new Float32Array(16), a00 = a[ 0], a01 = a[ 1], a02 = a[ 2], a03 = a[ 3], a10 = a[ 4], a11 = a[ 5], a12 = a[ 6], a13 = a[ 7], a20 = a[ 8], a21 = a[ 9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32, // Calculate the determinant det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return; } det = 1 / det; res[ 0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; res[ 1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; res[ 2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; res[ 3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; res[ 4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; res[ 5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; res[ 6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; res[ 7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; res[ 8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; res[ 9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; res[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; res[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; res[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; res[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; res[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; res[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return res; }; }()); GLX.texture = {}; GLX.texture.Image = function() { this.id = GL.createTexture(); GL.bindTexture(GL.TEXTURE_2D, this.id); //GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); //GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.bindTexture(GL.TEXTURE_2D, null); }; GLX.texture.Image.prototype = { clamp: function(image, maxSize) { if (image.width <= maxSize && image.height <= maxSize) { return image; } var w = maxSize, h = maxSize; var ratio = image.width/image.height; // TODO: if other dimension doesn't fit to POT after resize, there is still trouble if (ratio < 1) { w = Math.round(h*ratio); } else { h = Math.round(w/ratio); } var canvas = document.createElement('CANVAS'); canvas.width = w; canvas.height = h; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0, canvas.width, canvas.height); return canvas; }, load: function(url, callback) { var image = new Image(); image.crossOrigin = '*'; image.onload = function() { this.set(image); if (callback) { callback(image); } }.bind(this); image.onerror = function() { if (callback) { callback(); } }; image.src = url; return this; }, color: function(color) { GL.bindTexture(GL.TEXTURE_2D, this.id); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.LINEAR); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, 1, 1, 0, GL.RGBA, GL.UNSIGNED_BYTE, new Uint8Array([color[0]*255, color[1]*255, color[2]*255, (color[3] === undefined ? 1 : color[3])*255])); GL.bindTexture(GL.TEXTURE_2D, null); return this; }, set: function(image) { if (!this.id) { // texture has been destroyed return; } image = this.clamp(image, GL.getParameter(GL.MAX_TEXTURE_SIZE)); GL.bindTexture(GL.TEXTURE_2D, this.id); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR_MIPMAP_NEAREST); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.LINEAR); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, GL.RGBA, GL.UNSIGNED_BYTE, image); GL.generateMipmap(GL.TEXTURE_2D); if (GL.anisotropyExtension) { GL.texParameterf(GL.TEXTURE_2D, GL.anisotropyExtension.TEXTURE_MAX_ANISOTROPY_EXT, GL.anisotropyExtension.maxAnisotropyLevel); } GL.bindTexture(GL.TEXTURE_2D, null); return this; }, enable: function(index) { if (!this.id) { return; } GL.activeTexture(GL.TEXTURE0 + (index || 0)); GL.bindTexture(GL.TEXTURE_2D, this.id); return this; }, destroy: function() { GL.bindTexture(GL.TEXTURE_2D, null); GL.deleteTexture(this.id); this.id = null; } }; GLX.texture.Data = function(width, height, data, options) { //options = options || {}; this.id = GL.createTexture(); GL.bindTexture(GL.TEXTURE_2D, this.id); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.NEAREST); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.NEAREST); var bytes = null; if (data) { var length = width*height*4; bytes = new Uint8Array(length); bytes.set(data.subarray(0, length)); } GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, width, height, 0, GL.RGBA, GL.UNSIGNED_BYTE, bytes); GL.bindTexture(GL.TEXTURE_2D, null); }; GLX.texture.Data.prototype = { enable: function(index) { GL.activeTexture(GL.TEXTURE0 + (index || 0)); GL.bindTexture(GL.TEXTURE_2D, this.id); return this; }, destroy: function() { GL.bindTexture(GL.TEXTURE_2D, null); GL.deleteTexture(this.id); this.id = null; } }; return GLX; }()); // var vec2 = { len: function(a) { return Math.sqrt(a[0]*a[0] + a[1]*a[1]); }, add: function(a, b) { return [a[0]+b[0], a[1]+b[1]]; }, sub: function(a, b) { return [a[0]-b[0], a[1]-b[1]]; }, dot: function(a, b) { return a[1]*b[0] - a[0]*b[1]; }, scale: function(a, f) { return [a[0]*f, a[1]*f]; }, equals: function(a, b) { return (a[0] === b[0] && a[1] === b[1]); } }; var vec3 = { len: function(a) { return Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]); }, sub: function(a, b) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; }, unit: function(a) { var l = this.len(a); return [a[0]/l, a[1]/l, a[2]/l]; }, normal: function(a, b, c) { var d1 = this.sub(a, b); var d2 = this.sub(b, c); // normalized cross product of d1 and d2 return this.unit([ d1[1]*d2[2] - d1[2]*d2[1], d1[2]*d2[0] - d1[0]*d2[2], d1[0]*d2[1] - d1[1]*d2[0] ]); } }; var earcut = (function() { function earcut(data, holeIndices, dim) { dim = dim || 2; var hasHoles = holeIndices && holeIndices.length, outerLen = hasHoles ? holeIndices[0]*dim : data.length, outerNode = linkedList(data, 0, outerLen, dim, true), triangles = []; if (!outerNode) return triangles; var minX, minY, maxX, maxY, x, y, size; if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox if (data.length>80*dim) { minX = maxX = data[0]; minY = maxY = data[1]; for (var i = dim; imaxX) maxX = x; if (y>maxY) maxY = y; } // minX, minY and size are later used to transform coords into integers for z-order calculation size = Math.max(maxX - minX, maxY - minY); } earcutLinked(outerNode, triangles, dim, minX, minY, size); return triangles; } // create a circular doubly linked list from polygon points in the specified winding order function linkedList(data, start, end, dim, clockwise) { var i, last; if (clockwise === (signedArea(data, start, end, dim)>0)) { for (i = start; i=start; i -= dim) last = insertNode(i, data[i], data[i + 1], last); } if (last && equals(last, last.next)) { removeNode(last); last = last.next; } return last; } // eliminate colinear or duplicate points function filterPoints(start, end) { if (!start) return start; if (!end) end = start; var p = start, again; do { again = false; if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) { removeNode(p); p = end = p.prev; if (p === p.next) return null; again = true; } else { p = p.next; } } while (again || p !== end); return end; } // main ear slicing loop which triangulates a polygon (given as a linked list) function earcutLinked(ear, triangles, dim, minX, minY, size, pass) { if (!ear) return; // interlink polygon nodes in z-order if (!pass && size) indexCurve(ear, minX, minY, size); var stop = ear, prev, next; // iterate through ears, slicing them one by one while (ear.prev !== ear.next) { prev = ear.prev; next = ear.next; if (size ? isEarHashed(ear, minX, minY, size) : isEar(ear)) { // cut off the triangle triangles.push(prev.i/dim); triangles.push(ear.i/dim); triangles.push(next.i/dim); removeNode(ear); // skipping the next vertice leads to less sliver triangles ear = next.next; stop = next.next; continue; } ear = next; // if we looped through the whole remaining polygon and can't find any more ears if (ear === stop) { // try filtering points and slicing again if (!pass) { earcutLinked(filterPoints(ear), triangles, dim, minX, minY, size, 1); // if this didn't work, try curing all small self-intersections locally } else if (pass === 1) { ear = cureLocalIntersections(ear, triangles, dim); earcutLinked(ear, triangles, dim, minX, minY, size, 2); // as a last resort, try splitting the remaining polygon into two } else if (pass === 2) { splitEarcut(ear, triangles, dim, minX, minY, size); } break; } } } // check whether a polygon node forms a valid ear with adjacent nodes function isEar(ear) { var a = ear.prev, b = ear, c = ear.next; if (area(a, b, c)>=0) return false; // reflex, can't be an ear // now make sure we don't have other points inside the potential ear var p = ear.next.next; while (p !== ear.prev) { if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next)>=0) return false; p = p.next; } return true; } function isEarHashed(ear, minX, minY, size) { var a = ear.prev, b = ear, c = ear.next; if (area(a, b, c)>=0) return false; // reflex, can't be an ear // triangle bbox; min & max are calculated like this for speed var minTX = a.xb.x ? (a.x>c.x ? a.x : c.x) : (b.x>c.x ? b.x : c.x), maxTY = a.y>b.y ? (a.y>c.y ? a.y : c.y) : (b.y>c.y ? b.y : c.y); // z-order range for the current triangle bbox; var minZ = zOrder(minTX, minTY, minX, minY, size), maxZ = zOrder(maxTX, maxTY, minX, minY, size); // first look for points inside the triangle in increasing z-order var p = ear.nextZ; while (p && p.z<=maxZ) { if (p !== ear.prev && p !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next)>=0) return false; p = p.nextZ; } // then look for points in decreasing z-order p = ear.prevZ; while (p && p.z>=minZ) { if (p !== ear.prev && p !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next)>=0) return false; p = p.prevZ; } return true; } // go through all polygon nodes and cure small local self-intersections function cureLocalIntersections(start, triangles, dim) { var p = start; do { var a = p.prev, b = p.next.next; if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) { triangles.push(a.i/dim); triangles.push(p.i/dim); triangles.push(b.i/dim); // remove two nodes involved removeNode(p); removeNode(p.next); p = start = b; } p = p.next; } while (p !== start); return p; } // try splitting polygon into two and triangulate them independently function splitEarcut(start, triangles, dim, minX, minY, size) { // look for a valid diagonal that divides the polygon into two var a = start; do { var b = a.next.next; while (b !== a.prev) { if (a.i !== b.i && isValidDiagonal(a, b)) { // split the polygon in two by the diagonal var c = splitPolygon(a, b); // filter colinear points around the cuts a = filterPoints(a, a.next); c = filterPoints(c, c.next); // run earcut on each half earcutLinked(a, triangles, dim, minX, minY, size); earcutLinked(c, triangles, dim, minX, minY, size); return; } b = b.next; } a = a.next; } while (a !== start); } // link every hole into the outer loop, producing a single-ring polygon without holes function eliminateHoles(data, holeIndices, outerNode, dim) { var queue = [], i, len, start, end, list; for (i = 0, len = holeIndices.length; i=p.next.y) { var x = p.x + (hy - p.y)*(p.next.x - p.x)/(p.next.y - p.y); if (x<=hx && x>qx) { qx = x; if (x === hx) { if (hy === p.y) return p; if (hy === p.next.y) return p.next; } m = p.x=p.x && p.x>=mx && pointInTriangle(hym.x)) && locallyInside(p, hole)) { m = p; tanMin = tan; } } p = p.next; } return m; } // interlink polygon nodes in z-order function indexCurve(start, minX, minY, size) { var p = start; do { if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, size); p.prevZ = p.prev; p.nextZ = p.next; p = p.next; } while (p !== start); p.prevZ.nextZ = null; p.prevZ = null; sortLinked(p); } // Simon Tatham's linked list merge sort algorithm // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html function sortLinked(list) { var i, p, q, e, tail, numMerges, pSize, qSize, inSize = 1; do { p = list; list = null; tail = null; numMerges = 0; while (p) { numMerges++; q = p; pSize = 0; for (i = 0; i0 || (qSize>0 && q)) { if (pSize === 0) { e = q; q = q.nextZ; qSize--; } else if (qSize === 0 || !q) { e = p; p = p.nextZ; pSize--; } else if (p.z<=q.z) { e = p; p = p.nextZ; pSize--; } else { e = q; q = q.nextZ; qSize--; } if (tail) tail.nextZ = e; else list = e; e.prevZ = tail; tail = e; } p = q; } tail.nextZ = null; inSize *= 2; } while (numMerges>1); return list; } // z-order of a point given coords and size of the data bounding box function zOrder(x, y, minX, minY, size) { // coords are transformed into non-negative 15-bit integer range x = 32767*(x - minX)/size; y = 32767*(y - minY)/size; x = (x | (x<<8)) & 0x00FF00FF; x = (x | (x<<4)) & 0x0F0F0F0F; x = (x | (x<<2)) & 0x33333333; x = (x | (x<<1)) & 0x55555555; y = (y | (y<<8)) & 0x00FF00FF; y = (y | (y<<4)) & 0x0F0F0F0F; y = (y | (y<<2)) & 0x33333333; y = (y | (y<<1)) & 0x55555555; return x | (y<<1); } // find the leftmost node of a polygon ring function getLeftmost(start) { var p = start, leftmost = start; do { if (p.x=0 && (ax - px)*(by - py) - (bx - px)*(ay - py)>=0 && (bx - px)*(cy - py) - (cx - px)*(by - py)>=0; } // check if a diagonal between two polygon nodes is valid (lies in polygon interior) function isValidDiagonal(a, b) { return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b); } // signed area of a triangle function area(p, q, r) { return (q.y - p.y)*(r.x - q.x) - (q.x - p.x)*(r.y - q.y); } // check if two points are equal function equals(p1, p2) { return p1.x === p2.x && p1.y === p2.y; } // check if two segments intersect function intersects(p1, q1, p2, q2) { if ((equals(p1, q1) && equals(p2, q2)) || (equals(p1, q2) && equals(p2, q1))) return true; return area(p1, q1, p2)>0 !== area(p1, q1, q2)>0 && area(p2, q2, p1)>0 !== area(p2, q2, q1)>0; } // check if a polygon diagonal intersects any polygon segments function intersectsPolygon(a, b) { var p = a; do { if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && intersects(p, p.next, a, b)) return true; p = p.next; } while (p !== a); return false; } // check if a polygon diagonal is locally inside the polygon function locallyInside(a, b) { return area(a.prev, a, a.next)<0 ? area(a, b, a.next)>=0 && area(a, a.prev, b)>=0 : area(a, b, a.prev)<0 || area(a, a.next, b)<0; } // check if the middle point of a polygon diagonal is inside the polygon function middleInside(a, b) { var p = a, inside = false, px = (a.x + b.x)/2, py = (a.y + b.y)/2; do { if (((p.y>py) !== (p.next.y>py)) && (px<(p.next.x - p.x)*(py - p.y)/(p.next.y - p.y) + p.x)) inside = !inside; p = p.next; } while (p !== a); return inside; } // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; // if one belongs to the outer ring and another to a hole, it merges it into a single ring function splitPolygon(a, b) { var a2 = new Node(a.i, a.x, a.y), b2 = new Node(b.i, b.x, b.y), an = a.next, bp = b.prev; a.next = b; b.prev = a; a2.next = an; an.prev = a2; b2.next = a2; a2.prev = b2; bp.next = b2; b2.prev = bp; return b2; } // create a node and optionally link it with previous one (in a circular doubly linked list) function insertNode(i, x, y, last) { var p = new Node(i, x, y); if (!last) { p.prev = p; p.next = p; } else { p.next = last.next; p.prev = last; last.next.prev = p; last.next = p; } return p; } function removeNode(p) { p.next.prev = p.prev; p.prev.next = p.next; if (p.prevZ) p.prevZ.nextZ = p.nextZ; if (p.nextZ) p.nextZ.prevZ = p.prevZ; } function Node(i, x, y) { // vertice index in coordinates array this.i = i; // vertex coordinates this.x = x; this.y = y; // previous and next vertice nodes in a polygon ring this.prev = null; this.next = null; // z-order curve value this.z = null; // previous and next nodes in z-order this.prevZ = null; this.nextZ = null; // indicates whether this is a steiner point this.steiner = false; } // return a percentage difference between the polygon area and its triangulation area; // used to verify correctness of triangulation earcut.deviation = function(data, holeIndices, dim, triangles) { var hasHoles = holeIndices && holeIndices.length; var outerLen = hasHoles ? holeIndices[0]*dim : data.length; var i, len; var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim)); if (hasHoles) { for (i = 0, len = holeIndices.length; i0) { holeIndex += data[i - 1].length; result.holes.push(holeIndex); } } return result; }; return earcut; }(this)); function getSqDist(p1, p2) { var dx = p1[0] - p2[0], dy = p1[1] - p2[1]; return dx * dx + dy * dy; } function simplify(polygon, sqTolerance) { var prevPoint = polygon[0], newPoints = [prevPoint], point; for (var i = 1, len = polygon.length; i < len; i++) { point = polygon[i]; if (getSqDist(point, prevPoint) > sqTolerance) { newPoints.push(point); prevPoint = point; } } if (prevPoint !== point) { newPoints.push(point); } if (newPoints.length < 3) { return polygon; } return newPoints; } function getPolygonDirection(polygon) { var d, segmentLength = 0, maxSegmentLength = 0, maxSegment; var simplePolygon = simplify(polygon, 10); for (var i = 0, il = simplePolygon.length - 1; i maxSegmentLength) { maxSegmentLength = segmentLength; maxSegment = [simplePolygon[i], simplePolygon[i + 1]]; } } if (maxSegment === undefined) { return; } d = vec2.sub(maxSegment[1], maxSegment[0]); return [d[0]/maxSegmentLength, d[1]/maxSegmentLength]; } function getPolygonIntersections(polygon, line) { var res = [], segment, intersection; for (var i = 0, il = polygon.length-1; i < il; i++) { segment = [polygon[i], polygon[i+1]]; intersection = getLineIntersection(segment, line); if (intersection !== undefined) { res.push({ index:i, segment:segment }); } } return res; } function getLineIntersection(line1, line2) { if (vec2.equals(line1[0], line2[0]) || vec2.equals(line1[0], line2[1]) || vec2.equals(line1[1], line2[0]) || vec2.equals(line1[1], line2[1])) { return; } var d1 = vec2.sub(line1[1], line1[0]), d2 = vec2.sub(line2[1], line2[0]); // calculate dot product; // if dot product is close to 0, the lines are parallel var denom = vec2.dot(d1, d2); if (Math.abs(denom) < 1e-10) { return; } // calculate vector for connection between line1[0] and line2[0] var amc = vec2.sub(line2[0], line1[0]); // calculate t so that intersection is at line1[0]+t*v var t = vec2.dot(amc, d2)/denom; if (t<0 || t>1) { return; } // calculate s so that intersection is at line2[0]+t*q var s = vec2.dot(amc, d1)/denom; if (s<0 || s>1) { return; } return vec2.add(line1[0], vec2.scale(d1, t)); } // function getDistanceToSegment(point, line) { // var length = vec2.len(vec2.sub(line[1], line[0])); // if (length === 0) { // return vec2.len(vec2.sub(point, line[0])); // } // // var t = ((point[0]-line[0][0]) * (line[1][0]-line[0][0]) + (point[1]-line[0][1]) * (line[1][1]-line[0][1])) / length; // t = Math.max(0, Math.min(1, t)); // // var d = vec2.len(vec2.sub(point, vec2.add(line[0], vec2.sub(line[1], vec2.scale(point, t))))); // return Math.sqrt(d); // } function getDistanceToLine(a, line) { var r1 = line[0]; var r2 = line[1]; if (r1[0] === r2[0] && r1[1] === r2[1]) { return; } var m1 = (r2[1] - r1[1]) / (r2[0] - r1[0]); var b1 = r1[1] - (m1*r1[0]); if (m1 === 0) { return Math.abs(b1-a[1]); } if (m1 === Infinity){ return Math.abs(r1[0]-a[0]); } var m2 =- 1.0/m1; var b2 = a[1] - (m2*a[0]); var xs = (b2-b1)/(m1-m2); var ys = m1*xs+b1; var c1 = a[0]-xs; var c2 = a[1]-ys; return Math.sqrt(c1*c1+c2*c2); } function getSegmentCenter(seg) { return vec2.add(seg[0], vec2.scale(vec2.sub(seg[1], seg[0]), 0.5) ); } // TODO: handle inner rings function addRidgedRoof(buffers, properties, polygon, offset, dim, wallColor, roofColor) { offset = 0; // TODO var i, outerPolygon = polygon[0], direction, angle, rad; if (properties.roofRidgeDirection !== undefined) { angle = parseFloat(properties.roofRidgeDirection); } else if (properties.roofDirection !== undefined) { angle = parseFloat(properties.roofDirection); } if (!isNaN(angle)) { rad = angle*Math.PI/180; direction = [Math.sin(rad), Math.cos(rad)]; } else { direction = getPolygonDirection(outerPolygon); if (properties.roofOrientation && properties.roofOrientation === 'across') { direction = [-direction[1], direction[0]]; } } if (direction === undefined) { return FlatRoof(buffers, properties, polygon, dim, roofColor); } direction = vec2.scale(direction, 1000); // calculate the two outermost intersection indices of the // quasi-infinite ridge line with segments of the polygon var intersections = getPolygonIntersections(outerPolygon, [vec2.sub(dim.center, direction), vec2.add(dim.center, direction)]); // need at least two intersections if (intersections.length < 2) { return FlatRoof(buffers, properties, polygon, dim, roofColor); } // roof caps that are close to first and second vertex of the ridge var cap1 = intersections[0], cap2 = intersections[1]; // make sure, indices are in ascending order if (cap1.index > cap2.index) { var tmp = cap1; cap1 = cap2; cap2 = tmp; } // put ridge to the centers of the intersected segments cap1.center = getSegmentCenter(cap1.segment); cap2.center = getSegmentCenter(cap2.segment); if (offset === 0) { var ridge = [cap1.center, cap2.center], maxDistance = 0, distances = []; for (i = 0; i < outerPolygon.length; i++) { distances[i] = getDistanceToLine(outerPolygon[i], ridge); maxDistance = Math.max(maxDistance, distances[i]); } // modify vertical position of all points for (i = 0; i < outerPolygon.length; i++) { outerPolygon[i][2] = (1-distances[i]/maxDistance) * dim.roofHeight; } cap1.center[2] = dim.roofHeight; cap2.center[2] = dim.roofHeight; // create roof faces var roofFace1 = [cap1.center]; roofFace1 = roofFace1.concat(outerPolygon.slice(cap1.index+1, cap2.index+1)); roofFace1.push(cap2.center, cap1.center); split.polygon(buffers, [roofFace1], dim.roofZ, roofColor); var roofFace2 = [cap2.center]; roofFace2 = roofFace2.concat(outerPolygon.slice(cap2.index+1, outerPolygon.length-1)); roofFace2 = roofFace2.concat(outerPolygon.slice(0, cap1.index+1)); roofFace2.push(cap1.center, cap2.center); split.polygon(buffers, [roofFace2], dim.roofZ, roofColor); // create extra wall faces outerPolygon.splice(cap1.index+1, 0, cap1.center); outerPolygon.splice(cap2.index+2, 0, cap2.center); for (i = 0; i < outerPolygon.length-1; i++) { split.quad( buffers, [outerPolygon[i ][0], outerPolygon[i ][1],dim.roofZ+outerPolygon[i ][2]], [outerPolygon[i ][0], outerPolygon[i ][1],dim.roofZ], [outerPolygon[i+1][0], outerPolygon[i+1][1],dim.roofZ], [outerPolygon[i+1][0], outerPolygon[i+1][1],dim.roofZ+outerPolygon[i+1][2]], wallColor ); } } // // absolute distance of ridge to outline // var ridgeOffset = vec2.scale(vec2.sub(c2, c1), offset); // return [vec2.add(c1, ridgeOffset), vec2.sub(c2, ridgeOffset)]; } function addSkillionRoof(buffers, properties, polygon, dim, wallColor, roofColor) { var i, outerPolygon = polygon[0], direction, angle, rad; if (properties.roofSlopeDirection !== undefined) { angle = parseFloat(properties.roofSlopeDirection); if (!isNaN(angle)) { rad = angle*Math.PI/180; direction = [Math.sin(rad), Math.cos(rad)]; } } else if (properties.roofDirection !== undefined) { angle = parseFloat(properties.roofDirection); if (!isNaN(angle)) { rad = angle*Math.PI/180; direction = [Math.sin(rad), Math.cos(rad)]; } } else { direction = getPolygonDirection(outerPolygon); direction = [-direction[1], direction[0]]; if (properties.roofOrientation && properties.roofOrientation === 'across') { direction = [-direction[1], direction[0]]; } } if (direction === undefined) { return FlatRoof(buffers, properties, polygon, dim, roofColor); } direction = vec2.scale(direction, 1000); // get farthest intersection of polygon and slope line var intersections = getPolygonIntersections(outerPolygon, [vec2.sub(dim.center, direction), vec2.add(dim.center, direction)]), ridge, distance = 0, maxDistance = 0; for (i = 0; i < intersections.length; i++) { distance = getDistanceToLine(dim.center, intersections[i].segment); if (distance > maxDistance) { ridge = intersections[i].segment; maxDistance = distance; } } if (ridge === undefined) { return FlatRoof(buffers, properties, polygon, dim, roofColor); } maxDistance = 0; var distances = []; for (i = 0; i < outerPolygon.length; i++) { distances[i] = getDistanceToLine(outerPolygon[i], ridge); maxDistance = Math.max(maxDistance, distances[i]); } // modify vertical position of all points for (i = 0; i < outerPolygon.length; i++) { outerPolygon[i][2] = (1-distances[i]/maxDistance) * dim.roofHeight; } // create roof face split.polygon(buffers, [outerPolygon], dim.roofZ, roofColor); // create extra wall faces for (i = 0; i < outerPolygon.length-1; i++) { split.quad( buffers, [outerPolygon[i ][0], outerPolygon[i ][1],dim.roofZ+outerPolygon[i ][2]], [outerPolygon[i ][0], outerPolygon[i ][1],dim.roofZ], [outerPolygon[i+1][0], outerPolygon[i+1][1],dim.roofZ], [outerPolygon[i+1][0], outerPolygon[i+1][1],dim.roofZ+outerPolygon[i+1][2]], wallColor ); } } // this is also a fallback for failing roof calculation function FlatRoof(buffers, properties, polygon, dim, roofColor) { if (properties.shape === 'cylinder') { split.circle(buffers, dim.center, dim.radius, dim.roofZ, roofColor); } else { split.polygon(buffers, polygon, dim.roofZ, roofColor); } } /*** function HalfHippedRoof(tags, polygon) { RidgedRoof.call(this, tags, polygon, 1/6); this.cap1part = [ interpolateBetween(this.cap1[0], this.cap1[1], 0.5 - this.ridgeOffset/this.cap1.getLength()), interpolateBetween(this.cap1[0], this.cap1[1], 0.5 + this.ridgeOffset/this.cap1.getLength()) ]; this.cap2part = [ interpolateBetween(this.cap2[0], this.cap2[1], 0.5 - this.ridgeOffset/this.cap1.getLength()), interpolateBetween(this.cap2[0], this.cap2[1], 0.5 + this.ridgeOffset/this.cap1.getLength()) ]; } HalfHippedRoof.prototype = Object.create(RidgedRoof.prototype); HalfHippedRoof.prototype.getPolygon = function() { var outerPoly = this.polygon[0]; outerPoly = insertIntoPolygon(outerPoly, this.cap1part[0], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap1part[1], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap2part[0], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap2part[1], 0.2); return new PolygonWithHolesXZ(outerPoly.asSimplePolygon(), this.polygon.getHoles()); }; HalfHippedRoof.prototype.getInnerPoints = function() { return []; }; HalfHippedRoof.prototype.getInnerSegments = function() { return [this.ridge, [this.ridge[0], this.cap1part[0]], [this.ridge[0], this.cap1part[1]], [this.ridge[1], this.cap2part[0]], [this.ridge[1], this.cap2part[1]] ]; }; function GambrelRoof(tags, polygon) { RidgedRoof.call(this, tags, polygon, 0); this.cap1part = [ interpolateBetween(this.cap1[0], this.cap1[1], 1/6.0), interpolateBetween(this.cap1[0], this.cap1[1], 5/6.0) ]; this.cap2part = [ interpolateBetween(this.cap2[0], this.cap2[1], 1/6.0), interpolateBetween(this.cap2[0], this.cap2[1], 5/6.0) ]; } GambrelRoof.prototype = Object.create(RidgedRoof.prototype); GambrelRoof.prototype.getPolygon = function() { var outerPoly = this.polygon[0]; outerPoly = insertIntoPolygon(outerPoly, this.ridge[0], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.ridge[1], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap1part[0], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap1part[1], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap2part[0], 0.2); outerPoly = insertIntoPolygon(outerPoly, this.cap2part[1], 0.2); // TODO: add intersections of additional edges with outline? return new PolygonWithHolesXZ( outerPoly.asSimplePolygon(), this.polygon.getHoles() ); }; GambrelRoof.prototype.getInnerPoints = function() { return []; }; GambrelRoof.prototype.getInnerSegments = function() { return [this.ridge, [this.cap1part[0], this.cap2part[1]], [this.cap1part[1], this.cap2part[0]] ]; }; //************************************************************************************************* function RoundRoof() { RidgedRoof.call(this, 0); if (this.roofHeight] - The height above ground to place the model at * @param {String} [options.id] - An identifier for the object. This is used for getting info about the object later * @param {String} [options.color] - A color to apply to the model */ addOBJ: function(url, position, options) { return new mesh.OBJ(url, position, options); }, /** * A function that will be called on each feature, for modification before rendering * @callback OSMBuildings~modifierFunction * @param {String} id - The feature's id * @param {Object} properties - The feature's properties */ /** * Adds a GeoJSON layer to the map * @public * @param {String} url - URL of the GeoJSON file or a JavaScript Object representing a GeoJSON FeatureCollection * @param {Object} options - Options to apply to the GeoJSON being rendered * @param {Number} [options.scale=1] - Scale the model by this value before rendering * @param {Number} [options.rotation=0] - Rotate the model by this much before rendering * @param {Number} [options.elevation=] - The height above ground to place the model at * @param {String} [options.id] - An identifier for the object. This is used for getting info about the object later * @param {String} [options.color] - A color to apply to the model * @param {Number} [options.minZoom=14.5] - Minimum zoom level to show this feature, defaults to and limited by global minZoom * @param {Number} [options.maxZoom=maxZoom] - Maximum zoom level to show this feature, defaults to and limited by global maxZoom * @param {Boolean} [options.fadeIn=true] - Fade GeoJSON features; if `false`, then display immediately. */ addGeoJSON: function(url, options) { return new mesh.GeoJSON(url, options); }, // TODO: allow more data layers later on /** * Adds a GeoJSON tile layer, for rendering the 3D buildings * @public * @param {String} url - The URL of the GeoJSON tile server, in {@link https://github.com/OSMBuildings/OSMBuildings/blob/master/docs/server.md the correct format} * @param {Object} options * @param {Number} [options.fixedZoom=15] * @param {Object} [options.bounds] - Currently not used * @param {String} [options.color] - A color to apply to all features on this layer * @param {OSMBuildings~modifierFunction} [options.modifier] - DISCONTINUED. Use 'loadfeature' event instead. * @param {Number} [options.minZoom=14.5] - Minimum zoom level to show features from this layer, defaults to and limited by global minZoom * @param {Number} [options.maxZoom=maxZoom] - Maximum zoom level to show features from this layer, defaults to and limited by global maxZoom * @param {Boolean} [options.fadeIn=true] - Fade GeoJSON features; if `false`, then display immediately. */ addGeoJSONTiles: function(url, options) { options = options || {}; options.fixedZoom = options.fixedZoom || 15; APP.dataGrid = new Grid(url, data.Tile, options); return APP.dataGrid; }, /** * Adds a 2D map source, to render below the 3D buildings * @public * @param {String} url - The URL of the map server. This could be Mapbox, or {@link https://wiki.openstreetmap.org/wiki/Tiles any other tile server} that supports the right format * @param {Object} options * @param {Number} [options.fixedZoom] * @param {Object} [options.bounds] - Currently not used * @param {String} [options.color] - A color to apply to all features on this layer */ addMapTiles: function(url, options) { APP.basemapGrid = new Grid(url, basemap.Tile, options); return APP.basemapGrid; }, /** * Highlight a given feature by id. Currently, the highlight can only be applied to one feature. Set id = `null` in order to un-highlight * @public * @param {String} id - The feature's id. For OSM buildings, it's the OSM id. For other objects, it's whatever is defined in the options passed to it. * @param {String} highlightColor - An optional color string to be used for highlighting */ highlight: function(id, highlightColor) { render.Buildings.highlightId = id ? render.Picking.idToColor(id) : null; render.Buildings.highlightColor = id && highlightColor ? Color.parse(highlightColor).toArray() : HIGHLIGHT_COLOR; return APP; }, /** * A function that will be called on each feature, for modification before rendering * @callback OSMBuildings~selectorFunction * @param {String} id - The feature's id * @param {Object} data - The feature's data */ // TODO: check naming. show() suggests it affects the layer rather than objects on it /** * Sets a function that defines which objects to show on this layer * @public * @param {OSMBuildings~selectorFunction} selector - A function that will get run on each feature, and returns a boolean indicating whether or not to show the feature * @param {Integer} [duration=0] - How long to show the feature for */ show: function(selector, duration) { Filter.remove('hidden', selector, duration); return APP; }, // TODO: check naming. hide() suggests it affects the layer rather than objects on it /** * Sets a function that defines which objects to hide on this layer * @public * @param {OSMBuildings~selectorFunction} selector - A function that will get run on each feature, and returns a boolean indicating whether or not to hide the feature * @param {Integer} [duration=0] - How long to hide the feature for */ hide: function(selector, duration) { Filter.add('hidden', selector, duration); return APP; }, /** * A callback function for getTarget * @callback OSMBuildings~getTargetCallback * @param {Object} feature - The feature */ /** * Returns the feature from a position on the screen. Works asynchronous. * @public * @param {Integer} x - The x coordinate (in pixels) of position on the screen * @param {Integer} y - The y coordinate (in pixels) of position on the screen * @param {OSMBuildings~getTargetCallback} callback - A callback function that receives the object */ getTarget: function(x, y, callback) { // TODO: use promises here render.Picking.getTarget(x, y, callback); return APP; }, /** * A callback function for screnshot * @callback OSMBuildings~screenshotCallback * @param screenshot - The screenshot */ /** * Take a screenshot. Works asynchronous. * @public * @param {OSMBuildings~screenshotCallback} callback - A callback function that receives the screenshot */ screenshot: function(callback) { // TODO: use promises here render.screenshotCallback = callback; return APP; }, /** * @private */ _updateAttribution: function() { var attribution = []; if (APP.attribution) { attribution.push(APP.attribution); } // for (var i = 0; i < APP.layers.length; i++) { // if (APP.layers[i].attribution) { // attribution.push(APP.layers[i].attribution); // } // } APP._attribution.innerHTML = attribution.join(' · '); }, /** * @private */ _getStateFromUrl: function() { var query = location.search, state = {}; if (query) { query.substring(1).replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function($0, $1, $2) { if ($1) { state[$1] = $2; } }); } APP.setPosition((state.lat !== undefined && state.lon !== undefined) ? { latitude:state.lat, longitude:state.lon } : APP.position); APP.setZoom(state.zoom !== undefined ? state.zoom : APP.zoom); APP.setRotation(state.rotation !== undefined ? state.rotation : APP.rotation); APP.setTilt(state.tilt !== undefined ? state.tilt : APP.tilt); }, /** * @private */ _setStateToUrl: function() { if (!history.replaceState || APP.stateDebounce) { return; } APP.stateDebounce = setTimeout(function() { APP.stateDebounce = null; var params = []; params.push('lat=' + APP.position.latitude.toFixed(6)); params.push('lon=' + APP.position.longitude.toFixed(6)); params.push('zoom=' + APP.zoom.toFixed(1)); params.push('tilt=' + APP.tilt.toFixed(1)); params.push('rotation=' + APP.rotation.toFixed(1)); history.replaceState({}, '', '?' + params.join('&')); }, 1000); }, setDisabled: function(flag) { Events.disabled = !!flag; return APP; }, isDisabled: function() { return !!Events.disabled; }, /** * Returns geographical bounds of the current view * - since the bounds are always axis-aligned they will contain areas that are * not currently visible if the current view is not also axis-aligned. * - The bounds only contain the map area that OSMBuildings considers for rendering. * OSMBuildings has a rendering distance of about 3.5km, so the bounds will * never extend beyond that, even if the horizon is visible (in which case the * bounds would mathematically be infinite). * - the bounds only consider ground level. For example, buildings whose top * is seen at the lower edge of the screen, but whose footprint is outside * - The bounds only consider ground level. For example, buildings whose top * is seen at the lower edge of the screen, but whose footprint is outside * of the current view below the lower edge do not contribute to the bounds. * so their top may be visible and they may still be out of bounds. * @public * @returns {Array} bounding coordinates in unspecific order [{latitude,longitude},...] */ getBounds: function() { var viewQuad = render.getViewQuad(), res = []; for (var i in viewQuad) { res[i] = getPositionFromLocal(viewQuad[i]); } return res; }, /** * Sets the zoom level * @public * @param {Float} zoom - The new zoom level * @param {Object} e - **Not currently used** * @fires OSMBuildings#zoom * @fires OSMBuildings#change */ setZoom: function(zoom, e) { zoom = parseFloat(zoom); zoom = Math.max(zoom, APP.minZoom); zoom = Math.min(zoom, APP.maxZoom); if (APP.zoom !== zoom) { APP.zoom = zoom; /* if a screen position was given for which the geographic position displayed * should not change under the zoom */ if (e) { // FIXME: add code; this needs to take the current camera (rotation and // perspective) into account // NOTE: the old code (comment out below) only works for north-up // non-perspective views /* var dx = APP.container.offsetWidth/2 - e.clientX; var dy = APP.container.offsetHeight/2 - e.clientY; APP.center.x -= dx; APP.center.y -= dy; APP.center.x *= ratio; APP.center.y *= ratio; APP.center.x += dx; APP.center.y += dy;*/ } /** * Fired when the map is zoomed (in either direction) * @public * @fires OSMBuildings#zoom */ APP.emit('zoom', { zoom: zoom }); /** * Fired when the map is zoomed, tilted or panned * @public * @fires OSMBuildings#change */ APP.emit('change'); } return APP; }, /** * Gets current zoom level * @public * @returns {Number} zoom level */ getZoom: function() { return APP.zoom; }, /** * Sets the map's geographic position * @public * @param {Object} pos - The new position * @param {Float} pos.latitude * @param {Float} pos.longitude * @fires OSMBuildings#change */ setPosition: function(pos) { var lat = parseFloat(pos.latitude); var lon = parseFloat(pos.longitude); if (isNaN(lat) || isNaN(lon)) { return; } APP.position = { latitude: clamp(lat, -90, 90), longitude: clamp(lon, -180, 180) }; APP.emit('change'); return APP; }, /** * Returns the map's current geographic position * @public * @returns {Object} Geographic position {latitude,longitude} */ getPosition: function() { return APP.position; }, /** * Sets the map view's size in pixels * @public * @param {Object} size * @param {Integer} size.width * @param {Integer} size.height * @fires OSMBuildings#resize */ setSize: function(size) { if (size.width !== APP.width || size.height !== APP.height) { APP.width = size.width; APP.height = size.height; /** * Fired when the map is resized * @public * @fires OSMBuildings#resize */ APP.emit('resize', { width: APP.width, height: APP.height }); } return APP; }, /** * Returns the map's current view size in pixels * @public * @returns {Object} View size {width,height} */ getSize: function() { return { width: APP.width, height: APP.height }; }, /** * Set's the map's rotation * @public * @param {Float} rotation - The new rotation angle * @fires OSMBuildings#rotate * @fires OSMBuildings#change */ setRotation: function(rotation) { rotation = parseFloat(rotation)%360; if (APP.rotation !== rotation) { APP.rotation = rotation; /** * Fired when the map is rotated * @public * @fires OSMBuildings#rotate */ APP.emit('rotate', { rotation: rotation }); APP.emit('change'); } return APP; }, /** * Returns the map's current rotation * @public * @returns {Number} Rotation in degree */ getRotation: function() { return APP.rotation; }, /** * Sets the map's tilt * @public * @param {Float} tilt - The new tilt * @fires OSMBuildings#tilt * @fires OSMBuildings#change */ setTilt: function(tilt) { tilt = clamp(parseFloat(tilt), 0, 45); // bigger max increases shadow moire on base map if (APP.tilt !== tilt) { APP.tilt = tilt; /** * Fired when the map is tilted * @public * @fires OSMBuildings#tilt */ APP.emit('tilt', { tilt: tilt }); APP.emit('change'); } return APP; }, /** * Returns the map's current tilt * @public * @returns {Number} Tilt in degree */ getTilt: function() { return APP.tilt; }, /** * Destroys the map * @public */ destroy: function() { render.destroy(); // APP.basemapGrid.destroy(); // APP.dataGrid.destroy(); // TODO: when taking over an existing canvas, better don't destroy it here GLX.destroy(); data.Index.destroy(); APP.container.innerHTML = ''; } }; //***************************************************************************** if (typeof define === 'function') { define([], OSMBuildings); } else if (typeof module === 'object') { module.exports = OSMBuildings; } else { window.OSMBuildings = OSMBuildings; } // gesture polyfill adapted from https://raw.githubusercontent.com/seznam/JAK/master/lib/polyfills/gesturechange.js // MIT License /** * @private */ function add2(a, b) { return [a[0] + b[0], a[1] + b[1]]; } /** * @private */ function mul2scalar(a, f) { return [a[0]*f, a[1]*f]; } /** * @private */ function getEventPosition(e, offset) { return { x: e.clientX - offset.x, y: e.clientY - offset.y }; } /** * @private */ function getElementOffset(el) { if (el.getBoundingClientRect) { var box = el.getBoundingClientRect(); return { x:box.left, y:box.top }; } var res = { x:0, y:0 }; while(el.nodeType === 1) { res.x += el.offsetLeft; res.y += el.offsetTop; el = el.parentNode; } return res; } /** * @private */ function cancelEvent(e) { if (e.preventDefault) { e.preventDefault(); } //if (e.stopPropagation) { // e.stopPropagation(); //} e.returnValue = false; } /** * @private */ function addListener(target, type, fn) { target.addEventListener(type, fn, false); } /** * @private */ var Events = {}; /** * @private */ Events.disabled = false; /** * @private */ Events.init = function(container) { if ('ontouchstart' in window) { addListener(container, 'touchstart', onTouchStart); addListener(document, 'touchmove', onTouchMove); addListener(document, 'touchend', onTouchEnd); addListener(document, 'gesturechange', onGestureChange); } else { addListener(container, 'mousedown', onMouseDown); addListener(document, 'mousemove', onMouseMove); addListener(document, 'mouseup', onMouseUp); addListener(container, 'dblclick', onDoubleClick); addListener(container, 'mousewheel', onMouseWheel); addListener(container, 'DOMMouseScroll', onMouseWheel); } var resizeDebounce; addListener(window, 'resize', function() { if (resizeDebounce) { return; } resizeDebounce = setTimeout(function() { resizeDebounce = null; APP.setSize({ width:container.offsetWidth, height:container.offsetHeight }); }, 250); }); //*************************************************************************** var prevX = 0, prevY = 0, startX = 0, startY = 0, startZoom = 0, startOffset, prevRotation = 0, prevTilt = 0, pointerIsDown = false; function onDoubleClick(e) { cancelEvent(e); if (!Events.disabled) { APP.setZoom(APP.zoom + 1, e); } var pos = getEventPosition(e, getElementOffset(e.target)); APP.emit('doubleclick', { x:pos.x, y:pos.y, button:e.button }); } function onMouseDown(e) { cancelEvent(e); if (e.button > 1) { return; } startZoom = APP.zoom; prevRotation = APP.rotation; prevTilt = APP.tilt; startOffset = getElementOffset(e.target); var pos = getEventPosition(e, startOffset); startX = prevX = pos.x; startY = prevY = pos.y; pointerIsDown = true; APP.emit('pointerdown', { x: pos.x, y: pos.y, button: e.button }); } function onMouseMove(e) { var pos; if (!pointerIsDown) { pos = getEventPosition(e, getElementOffset(e.target)); } else { if (e.button === 0 && !e.altKey) { moveMap(e, startOffset); } else { rotateMap(e, startOffset); } pos = getEventPosition(e, startOffset); prevX = pos.x; prevY = pos.y; } APP.emit('pointermove', { x: pos.x, y: pos.y }); } function onMouseUp(e) { // prevents clicks on other page elements if (!pointerIsDown) { return; } var pos = getEventPosition(e, startOffset); if (e.button === 0 && !e.altKey) { if (Math.abs(pos.x - startX)>5 || Math.abs(pos.y - startY)>5) { moveMap(e, startOffset); } } else { rotateMap(e, startOffset); } pointerIsDown = false; APP.emit('pointerup', { x: pos.x, y: pos.y, button: e.button }); } function onMouseWheel(e) { cancelEvent(e); var delta = 0; if (e.wheelDeltaY) { delta = e.wheelDeltaY; } else if (e.wheelDelta) { delta = e.wheelDelta; } else if (e.detail) { delta = -e.detail; } if (!Events.disabled) { var adjust = 0.2*(delta>0 ? 1 : delta<0 ? -1 : 0); APP.setZoom(APP.zoom + adjust, e); } // we don't emit mousewheel here as we don't want to run into a loop of death } //*************************************************************************** function moveMap(e, offset) { if (Events.disabled) { return; } // FIXME: make movement exact, i.e. make the position that // appeared at (prevX, prevY) before appear at (e.offsetX, e.offsetY) now. // the constant 0.86 was chosen experimentally for the map movement to be // "pinned" to the cursor movement when the map is shown top-down var scale = 0.86 * Math.pow(2, -APP.zoom), lonScale = 1/Math.cos( APP.position.latitude/ 180 * Math.PI), pos = getEventPosition(e, offset), dx = pos.x - prevX, dy = pos.y - prevY, angle = APP.rotation * Math.PI/180, vRight = [ Math.cos(angle), Math.sin(angle)], vForward = [ Math.cos(angle - Math.PI/2), Math.sin(angle - Math.PI/2)], dir = add2(mul2scalar(vRight, dx), mul2scalar(vForward, -dy)); var newPosition = { longitude: APP.position.longitude - dir[0] * scale*lonScale, latitude: APP.position.latitude + dir[1] * scale }; APP.setPosition(newPosition); APP.emit('move', newPosition); } function rotateMap(e, offset) { if (Events.disabled) { return; } var pos = getEventPosition(e, offset); prevRotation += (pos.x - prevX)*(360/innerWidth); prevTilt -= (pos.y - prevY)*(360/innerHeight); APP.setRotation(prevRotation); APP.setTilt(prevTilt); } //*************************************************************************** var dist1 = 0, angle1 = 0, gestureStarted = false; function emitGestureChange(e) { var t1 = e.touches[0], t2 = e.touches[1], dx = t1.clientX - t2.clientX, dy = t1.clientY - t2.clientY, dist2 = dx*dx + dy*dy, angle2 = Math.atan2(dy, dx); onGestureChange({ rotation: ((angle2 - angle1)*(180/Math.PI))%360, scale: Math.sqrt(dist2/dist1) }); } function onTouchStart(e) { pointerIsDown = true; cancelEvent(e); // gesturechange polyfill if (e.touches.length === 2 && !('ongesturechange' in window)) { var t1 = e.touches[0]; var t2 = e.touches[1]; var dx = t1.clientX - t2.clientX; var dy = t1.clientY - t2.clientY; dist1 = dx*dx + dy*dy; angle1 = Math.atan2(dy,dx); gestureStarted = true; } startZoom = APP.zoom; prevRotation = APP.rotation; prevTilt = APP.tilt; if (e.touches.length) { e = e.touches[0]; } startOffset = getElementOffset(e.target); var pos = getEventPosition(e, startOffset); startX = prevX = pos.x; startY = prevY = pos.y; APP.emit('pointerdown', { x: pos.x, y: pos.y, button: 0 }); } function onTouchMove(e) { if (!pointerIsDown) { return; } var pos = getEventPosition(e.touches[0], startOffset); if (e.touches.length>1) { APP.setTilt(prevTilt + (prevY - pos.y)*(360/innerHeight)); prevTilt = APP.tilt; // gesturechange polyfill if (!('ongesturechange' in window)) { emitGestureChange(e); } } else { moveMap(e.touches[0], startOffset); APP.emit('pointermove', { x: pos.x, y: pos.y }); } prevX = pos.x; prevY = pos.y; } function onTouchEnd(e) { if (!pointerIsDown) { return; } // gesturechange polyfill gestureStarted = false; if (e.touches.length === 0) { pointerIsDown = false; APP.emit('pointerup', { x: prevX, y: prevY, button: 0 }); } else if (e.touches.length === 1) { // There is one touch currently on the surface => gesture ended. Prepare for continued single touch move var pos = getEventPosition(e.touches[0], startOffset); prevX = pos.x; prevY = pos.y; } } function onGestureChange(e) { if (!pointerIsDown) { return; } cancelEvent(e); if (!Events.disabled) { APP.setZoom(startZoom + (e.scale - 1)); APP.setRotation(prevRotation - e.rotation); } APP.emit('gesture', e); } }; var Activity = {}; // TODO: could turn into a public loading handler // OSMB.loader - stop(), start(), isBusy(), events.. (function() { var count = 0; var debounce; Activity.setBusy = function() { //console.log('setBusy', count); if (!count) { if (debounce) { clearTimeout(debounce); debounce = null; } else { /** * Fired when data loading starts * @fires OSMBuildings#busy */ APP.emit('busy'); } } count++; }; Activity.setIdle = function() { if (!count) { return; } count--; if (!count) { debounce = setTimeout(function() { debounce = null; /** * Fired when data loading ends * @fires OSMBuildings#idle */ APP.emit('idle'); }, 33); } //console.log('setIdle', count); }; Activity.isBusy = function() { return !!count; }; }()); var TILE_SIZE = 256; var DEFAULT_HEIGHT = 10; var HEIGHT_SCALE = 1.0; var MIN_ZOOM = 14.5; var MAX_ZOOM = 22; var MAX_USED_ZOOM_LEVEL = 25; var DEFAULT_COLOR = Color.parse('rgb(220, 210, 200)').toArray(); var HIGHLIGHT_COLOR = Color.parse('#f08000').toArray(); // #E8E0D8 is the background color of the current OSMBuildings map layer, // and thus a good fog color to blend map tiles and buildings close to the horizont into var FOG_COLOR = '#e8e0d8'; //var FOG_COLOR = '#f0f8ff'; var BACKGROUND_COLOR = '#efe8e0'; var document = window.document; var EARTH_RADIUS_IN_METERS = 6378137; var EARTH_CIRCUMFERENCE_IN_METERS = EARTH_RADIUS_IN_METERS * Math.PI * 2; var METERS_PER_DEGREE_LATITUDE = EARTH_CIRCUMFERENCE_IN_METERS / 360; /* the height of the skywall in meters */ var SKYWALL_HEIGHT = 2000.0; /* For shadow mapping, the camera rendering the scene as seen by the sun has * to cover everything that's also visible to the user. For this to work * reliably, we have to make assumptions on how high (in [m]) the buildings * can become. * Note: using a lower-than-accurate value will lead to buildings parts at the * edge of the viewport to have incorrect shadows. Using a higher-than-necessary * value will lead to an unnecessarily large view area and thus to poor shadow * resolution. */ var SHADOW_MAP_MAX_BUILDING_HEIGHT = 100; /* for shadow mapping, the scene needs to be rendered into a depth map as seen * by the light source. This rendering can have arbitrary dimensions - * they need not be related to the visible viewport size in any way. The higher * the resolution (width and height) for this depth map the smaller are * the visual artifacts introduced by shadow mapping. But increasing the * shadow depth map size impacts rendering performance */ var SHADOW_DEPTH_MAP_SIZE = 2048; //the building wall texture as a data url var BUILDING_TEXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQMAAACQp+OdAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wwCCAUQLpaUSQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAGUExURebm5v///zFES9kAAAAcSURBVCjPY/gPBQyUMh4wAAH/KAPCoFaoDnYGAAKtZsamTRFlAAAAAElFTkSuQmCC'; // TODO: introduce promises var Request = {}; (function() { function load(url, callback) { var req = new XMLHttpRequest(); req.onreadystatechange = function() { if (req.readyState !== 4) { return; } if (!req.status || req.status<200 || req.status>299) { return; } callback(req); }; req.open('GET', url); req.send(null); return { abort: function() { req.abort(); } }; } //*************************************************************************** Request.getText = function(url, callback) { return load(url, function(res) { if (res.responseText !== undefined) { callback(res.responseText); } }); }; Request.getXML = function(url, callback) { return load(url, function(res) { if (res.responseXML !== undefined) { callback(res.responseXML); } }); }; Request.getJSON = function(url, callback) { return load(url, function(res) { if (res.responseText) { var json; try { json = JSON.parse(res.responseText); } catch(ex) { console.warn('Could not parse JSON from '+ url +'\n'+ ex.message); } callback(json); } }); }; Request.destroy = function() {}; }()); /*function project(latitude, longitude, worldSize) { var x = longitude/360 + 0.5, y = Math.min(1, Math.max(0, 0.5 - (Math.log(Math.tan((Math.PI/4) + (Math.PI/2)*latitude/180)) / Math.PI) / 2)); return { x: x*worldSize, y: y*worldSize }; } function unproject(x, y, worldSize) { x /= worldSize; y /= worldSize; return { latitude: (2 * Math.atan(Math.exp(Math.PI * (1 - 2*y))) - Math.PI/2) * (180/Math.PI), longitude: x*360 - 180 }; }*/ function pattern(str, param) { return str.replace(/\{(\w+)\}/g, function(tag, key) { return param[key] || tag; }); } function assert(condition, message) { if (!condition) { throw message; } } /* returns a new list of points based on 'points' where the 3rd coordinate in * each entry is set to 'zValue' */ function substituteZCoordinate(points, zValue) { var res = []; for (var i in points) { res.push( [points[i][0], points[i][1], zValue] ); } return res; } function clamp(value, min, max) { return Math.min(max, Math.max(value, min)); } var Grid = function(source, tileClass, options) { this.tiles = {}; this.buffer = 1; this.source = source; this.tileClass = tileClass; options = options || {}; this.bounds = options.bounds; this.fixedZoom = options.fixedZoom; this.tileOptions = { color:options.color, fadeIn:options.fadeIn }; this.minZoom = Math.max(parseFloat(options.minZoom || MIN_ZOOM), APP.minZoom); this.maxZoom = Math.min(parseFloat(options.maxZoom || MAX_ZOOM), APP.maxZoom); if (this.maxZoom < this.minZoom) { this.minZoom = MIN_ZOOM; this.maxZoom = MAX_ZOOM; } APP.on('change', this._onChange = function() { this.update(500); }.bind(this)); APP.on('resize', this._onResize = this.update.bind(this)); this.update(); }; Grid.prototype = { // strategy: start loading after delay (ms), skip any attempts until then // effectively loads in intervals during movement update: function(delay) { if (APP.zoom < this.minZoom || APP.zoom > this.maxZoom) { return; } if (!delay) { this.loadTiles(); return; } if (!this.debounce) { this.debounce = setTimeout(function() { this.debounce = null; this.loadTiles(); }.bind(this), delay); } }, getURL: function(x, y, z) { var s = 'abcd'[(x+y) % 4]; return pattern(this.source, { s:s, x:x, y:y, z:z }); }, getClosestTiles: function(tileList, referencePoint) { tileList.sort(function(a, b) { // tile coordinates correspond to the tile's upper left corner, but for // the distance computation we should rather use their center; hence the 0.5 offsets var distA = Math.pow(a[0] + 0.5 - referencePoint[0], 2.0) + Math.pow(a[1] + 0.5 - referencePoint[1], 2.0); var distB = Math.pow(b[0] + 0.5 - referencePoint[0], 2.0) + Math.pow(b[1] + 0.5 - referencePoint[1], 2.0); return distA > distB; }); var prevX, prevY; // removes duplicates return tileList.filter(function(tile) { if (tile[0] === prevX && tile[1] === prevY) { return false; } prevX = tile[0]; prevY = tile[1]; return true; }); }, /* Returns a set of tiles based on 'tiles' (at zoom level 'zoom'), * but with those tiles recursively replaced by their respective parent tile * (tile from zoom level 'zoom'-1 that contains 'tile') for which said parent * tile covers less than 'pixelAreaThreshold' pixels on screen based on the * current view-projection matrix. * * The returned tile set is duplicate-free even if there were duplicates in * 'tiles' and even if multiple tiles from 'tiles' got replaced by the same parent. */ mergeTiles: function(tiles, zoom, pixelAreaThreshold) { var parentTiles = {}; var tileSet = {}; var tileList = []; var key; // if there is no parent zoom level if (zoom === 0 || zoom <= this.minZoom) { for (key in tiles) { tiles[key][2] = zoom; } return tiles; } for (key in tiles) { var tile = tiles[key]; var parentX = (tile[0] <<0) / 2; var parentY = (tile[1] <<0) / 2; if (parentTiles[ [parentX, parentY] ] === undefined) { //parent tile screen size unknown var numParentScreenPixels = getTileSizeOnScreen(parentX, parentY, zoom-1, render.viewProjMatrix); parentTiles[ [parentX, parentY] ] = (numParentScreenPixels < pixelAreaThreshold); } if (! parentTiles[ [parentX, parentY] ]) { //won't be replaced by a parent tile -->keep if (tileSet[ [tile[0], tile[1]] ] === undefined) { //remove duplicates tileSet[ [tile[0], tile[1]]] = true; tileList.push( [tile[0], tile[1], zoom]); } } } var parentTileList = []; for (key in parentTiles) { if (parentTiles[key]) { var parentTile = key.split(','); parentTileList.push( [parseInt(parentTile[0]), parseInt(parentTile[1]), zoom-1]); } } if (parentTileList.length > 0) { parentTileList = this.mergeTiles(parentTileList, zoom - 1, pixelAreaThreshold); } return tileList.concat(parentTileList); }, loadTiles: function() { var zoom = Math.round(this.fixedZoom || APP.zoom); // TODO: if there are user defined bounds for this layer, respect these too // if (this.fixedBounds) { // var // min = project(this.bounds.s, this.bounds.w, 1<'line2'. * based on http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html */ /* function getDistancePointLine2( line1, line2, p) { //v: a unit-length vector perpendicular to the line; var v = norm2( [ line2[1] - line1[1], line1[0] - line2[0] ] ); var r = sub2( line1, p); return Math.abs(dot2(v, r)); } */ /* given a pixel's (integer) position through which the line 'segmentStart' -> * 'segmentEnd' passes, this method returns the one neighboring pixel of * 'currentPixel' that would be traversed next if the line is followed in * the direction from 'segmentStart' to 'segmentEnd' (even if the next point * would lie beyond 'segmentEnd'. ) */ function getNextPixel(segmentStart, segmentEnd, currentPixel) { var vInc = [segmentStart[0] < segmentEnd[0] ? 1 : -1, segmentStart[1] < segmentEnd[1] ? 1 : -1]; var nextX = currentPixel[0] + (segmentStart[0] < segmentEnd[0] ? +1 : 0); var nextY = currentPixel[1] + (segmentStart[1] < segmentEnd[1] ? +1 : 0); // position of the edge to the next pixel on the line 'segmentStart'->'segmentEnd' var alphaX = (nextX - segmentStart[0])/ (segmentEnd[0] - segmentStart[0]); var alphaY = (nextY - segmentStart[1])/ (segmentEnd[1] - segmentStart[1]); // neither value is valid if ((alphaX <= 0.0 || alphaX > 1.0) && (alphaY <= 0.0 || alphaY > 1.0)) { return [undefined, undefined]; } if (alphaX <= 0.0 || alphaX > 1.0) { // only alphaY is valid return [currentPixel[0], currentPixel[1] + vInc[1]]; } if (alphaY <= 0.0 || alphaY > 1.0) { // only alphaX is valid return [currentPixel[0] + vInc[0], currentPixel[1]]; } return alphaX < alphaY ? [currentPixel[0]+vInc[0], currentPixel[1]] : [currentPixel[0], currentPixel[1] + vInc[1]]; } /* returns all pixels that are at least partially covered by the triangle * p1-p2-p3. * Note: the returned array of pixels *will* contain duplicates that may need * to be removed. */ function rasterTriangle(p1, p2, p3) { var points = [p1, p2, p3]; points.sort(function(p, q) { return p[1] < q[1]; }); p1 = points[0]; p2 = points[1]; p3 = points[2]; if (p1[1] == p2[1]) return rasterFlatTriangle( p1, p2, p3); if (p2[1] == p3[1]) return rasterFlatTriangle( p2, p3, p1); var alpha = (p2[1] - p1[1]) / (p3[1] - p1[1]); //point on the line p1->p3 with the same y-value as p2 var p4 = [p1[0] + alpha*(p3[0]-p1[0]), p2[1]]; /* P3 * |\ * | \ * P4--P2 * | / * |/ * P1 * */ return rasterFlatTriangle(p4, p2, p1).concat(rasterFlatTriangle(p4, p2, p3)); } /* Returns all pixels that are at least partially covered by the triangle * flat0-flat1-other, where the points flat0 and flat1 need to have the * same y-value. This method is used internally for rasterTriangle(), which * splits a general triangle into two flat triangles, and calls this method * for both parts. * Note: the returned array of pixels will contain duplicates. * * other * | \_ * | \_ * | \_ * f0/f1--f1/f0 */ function rasterFlatTriangle( flat0, flat1, other ) { //console.log("RFT:\n%s\n%s\n%s", String(flat0), String(flat1), String(other)); var points = []; assert(flat0[1] === flat1[1], 'not a flat triangle'); assert(other[1] !== flat0[1], 'not a triangle'); assert(flat0[0] !== flat1[0], 'not a triangle'); if (flat0[0] > flat1[0]) //guarantees that flat0 is always left of flat1 { var tmp = flat0; flat0 = flat1; flat1 = tmp; } var leftRasterPos = [other[0] <<0, other[1] <<0]; var rightRasterPos = leftRasterPos.slice(0); points.push(leftRasterPos.slice(0)); var yDir = other[1] < flat0[1] ? +1 : -1; var yStart = leftRasterPos[1]; var yBeyond= (flat0[1] <<0) + yDir; var prevLeftRasterPos; var prevRightRasterPos; for (var y = yStart; (y*yDir) < (yBeyond*yDir); y+= yDir) { do { points.push( leftRasterPos.slice(0)); prevLeftRasterPos = leftRasterPos; leftRasterPos = getNextPixel(other, flat0, leftRasterPos); } while (leftRasterPos[1]*yDir <= y*yDir); leftRasterPos = prevLeftRasterPos; do { points.push( rightRasterPos.slice(0)); prevRightRasterPos = rightRasterPos; rightRasterPos = getNextPixel(other, flat1, rightRasterPos); } while (rightRasterPos[1]*yDir <= y*yDir); rightRasterPos = prevRightRasterPos; for (var x = leftRasterPos[0]; x <= rightRasterPos[0]; x++) { points.push([x, y]); } } return points; } /* Returns an array of all pixels that are at least partially covered by the * convex quadrilateral 'quad'. If the passed quadrilateral is not convex, * then the return value of this method is undefined. */ function rasterConvexQuad(quad) { assert(quad.length == 4, 'Error: Quadrilateral with more or less than four vertices'); var res1 = rasterTriangle(quad[0], quad[1], quad[2]); var res2 = rasterTriangle(quad[0], quad[2], quad[3]); return res1.concat(res2); } // computes the normal vector of the triangle a-b-c function normal(a, b, c) { var d1 = sub3(a, b); var d2 = sub3(b, c); // normalized cross product of d1 and d2. return norm3([ d1[1]*d2[2] - d1[2]*d2[1], d1[2]*d2[0] - d1[0]*d2[2], d1[0]*d2[1] - d1[1]*d2[0] ]); } /* returns the quadrilateral part of the XY plane that is currently visible on * screen. The quad is returned in tile coordinates for tile zoom level * 'tileZoomLevel', and thus can directly be used to determine which basemap * and geometry tiles need to be loaded. * Note: if the horizon is level (as should usually be the case for * OSMBuildings) then said quad is also a trapezoid. */ function getViewQuad(viewProjectionMatrix, maxFarEdgeDistance, viewDirOnMap) { /* maximum distance from the map center at which * geometry is still visible */ //console.log("FMED:", MAX_FAR_EDGE_DISTANCE); var inverse = GLX.Matrix.invert(viewProjectionMatrix); var vBottomLeft = getIntersectionWithXYPlane(-1, -1, inverse); var vBottomRight = getIntersectionWithXYPlane( 1, -1, inverse); var vTopRight = getIntersectionWithXYPlane( 1, 1, inverse); var vTopLeft = getIntersectionWithXYPlane(-1, 1, inverse); /* If even the lower edge of the screen does not intersect with the map plane, * then the map plane is not visible at all. * (Or somebody screwed up the projection matrix, putting the view upside-down * or something. But in any case we won't attempt to create a view rectangle). */ if (!vBottomLeft || !vBottomRight) { return; } var vLeftDir, vRightDir, vLeftPoint, vRightPoint; var f; /* The lower screen edge shows the map layer, but the upper one does not. * This usually happens when the camera is close to parallel to the ground * so that the upper screen edge lies above the horizon. This is not a bug * and can legitimately happen. But from a theoretical standpoint, this means * that the view 'trapezoid' stretches infinitely toward the horizon. Since this * is not a practically useful result - though formally correct - we instead * manually bound that area.*/ if (!vTopLeft || !vTopRight) { /* point on the left screen edge with the same y-value as the map center*/ vLeftPoint = getIntersectionWithXYPlane(-1, -0.9, inverse); vLeftDir = norm2(sub2( vLeftPoint, vBottomLeft)); f = dot2(vLeftDir, viewDirOnMap); vTopLeft = add2( vBottomLeft, mul2scalar(vLeftDir, maxFarEdgeDistance/f)); vRightPoint = getIntersectionWithXYPlane( 1, -0.9, inverse); vRightDir = norm2(sub2(vRightPoint, vBottomRight)); f = dot2(vRightDir, viewDirOnMap); vTopRight = add2( vBottomRight, mul2scalar(vRightDir, maxFarEdgeDistance/f)); } /* if vTopLeft is further than maxFarEdgeDistance away vertically from the lower edge, * move it closer. */ if (dot2( viewDirOnMap, sub2(vTopLeft, vBottomLeft)) > maxFarEdgeDistance) { vLeftDir = norm2(sub2( vTopLeft, vBottomLeft)); f = dot2(vLeftDir, viewDirOnMap); vTopLeft = add2( vBottomLeft, mul2scalar(vLeftDir, maxFarEdgeDistance/f)); } /* dito for vTopRight*/ if (dot2( viewDirOnMap, sub2(vTopRight, vBottomRight)) > maxFarEdgeDistance) { vRightDir = norm2(sub2( vTopRight, vBottomRight)); f = dot2(vRightDir, viewDirOnMap); vTopRight = add2( vBottomRight, mul2scalar(vRightDir, maxFarEdgeDistance/f)); } return [vBottomLeft, vBottomRight, vTopRight, vTopLeft]; } /* Returns an orthographic projection matrix whose view rectangle contains all * points of 'points' when watched from the position given by targetViewMatrix. * The depth range of the returned matrix is [near, far]. * The 'points' are given as euclidean coordinates in [m] distance to the * current reference point (APP.position). */ function getCoveringOrthoProjection(points, targetViewMatrix, near, far, height) { var p0 = transformVec3(targetViewMatrix.data, points[0]); var left = p0[0]; var right= p0[0]; var top = p0[1]; var bottom=p0[1]; for (var i = 0; i < points.length; i++) { var p = transformVec3(targetViewMatrix.data, points[i]); left = Math.min( left, p[0]); right= Math.max( right, p[0]); top = Math.max( top, p[1]); bottom=Math.min( bottom,p[1]); } return new GLX.Matrix.Ortho(left, right, top, bottom, near, far); } /* transforms the 3D vector 'v' according to the transformation matrix 'm'. * Internally, the vector 'v' is interpreted as a 4D vector * (v[0], v[1], v[2], 1.0) in homogenous coordinates. The transformation is * performed on that vector, yielding a 4D homogenous result vector. That * vector is then converted back to a 3D Euler coordinates by dividing * its first three components each by its fourth component */ function transformVec3(m, v) { var x = v[0]*m[0] + v[1]*m[4] + v[2]*m[8] + m[12]; var y = v[0]*m[1] + v[1]*m[5] + v[2]*m[9] + m[13]; var z = v[0]*m[2] + v[1]*m[6] + v[2]*m[10] + m[14]; var w = v[0]*m[3] + v[1]*m[7] + v[2]*m[11] + m[15]; return [x/w, y/w, z/w]; //convert homogenous to Euler coordinates } /* returns the point (in OSMBuildings' local coordinates) on the XY plane (z==0) * that would be drawn at viewport position (screenNdcX, screenNdcY). * That viewport position is given in normalized device coordinates, i.e. * x==-1.0 is the left screen edge, x==+1.0 is the right one, y==-1.0 is the lower * screen edge and y==+1.0 is the upper one. */ function getIntersectionWithXYPlane(screenNdcX, screenNdcY, inverseTransform) { var v1 = transformVec3(inverseTransform, [screenNdcX, screenNdcY, 0]); var v2 = transformVec3(inverseTransform, [screenNdcX, screenNdcY, 1]); // direction vector from v1 to v2 var vDir = sub3(v2, v1); if (vDir[2] >= 0) // ray would not intersect with the plane { return; } /* ray equation for all world-space points 'p' lying on the screen-space NDC position * (screenNdcX, screenNdcY) is: p = v1 + λ*vDirNorm * For the intersection with the xy-plane (-> z=0) holds: v1[2] + λ*vDirNorm[2] = p[2] = 0.0. * Rearranged, this reads: */ var lambda = -v1[2]/vDir[2]; var pos = add3( v1, mul3scalar(vDir, lambda)); return [pos[0], pos[1]]; // z==0 } /* Returns: the number of screen pixels that would be covered by the tile * tileZoom/tileX/tileY *if* the screen would not end at the viewport * edges. The intended use of this method is to return a measure of * how detailed the tile should be rendered. * Note: This method does not clip the tile to the viewport. So the number * returned will be larger than the number of screen pixels covered iff. * the tile intersects with a viewport edge. */ function getTileSizeOnScreen(tileX, tileY, tileZoom, viewProjMatrix) { var metersPerDegreeLongitude = METERS_PER_DEGREE_LATITUDE * Math.cos(APP.position.latitude / 180 * Math.PI); var tileLon = tile2lon(tileX, tileZoom); var tileLat = tile2lat(tileY, tileZoom); var modelMatrix = new GLX.Matrix(); modelMatrix.translate( (tileLon - APP.position.longitude)* metersPerDegreeLongitude, -(tileLat - APP.position.latitude) * METERS_PER_DEGREE_LATITUDE, 0); var size = getTileSizeInMeters( APP.position.latitude, tileZoom); var mvpMatrix = GLX.Matrix.multiply(modelMatrix, viewProjMatrix); var tl = transformVec3(mvpMatrix, [0 , 0 , 0]); var tr = transformVec3(mvpMatrix, [size, 0 , 0]); var bl = transformVec3(mvpMatrix, [0 , size, 0]); var br = transformVec3(mvpMatrix, [size, size, 0]); var verts = [tl, tr, bl, br]; for (var i in verts) { // transformation from NDC [-1..1] to viewport [0.. width/height] coordinates verts[i][0] = (verts[i][0] + 1.0) / 2.0 * APP.width; verts[i][1] = (verts[i][1] + 1.0) / 2.0 * APP.height; } return getConvexQuadArea( [tl, tr, br, bl]); } function getTriangleArea(p1, p2, p3) { //triangle edge lengths var a = len2(sub2( p1, p2)); var b = len2(sub2( p1, p3)); var c = len2(sub2( p2, p3)); //Heron's formula var s = 0.5 * (a+b+c); return Math.sqrt( s * (s-a) * (s-b) * (s-c)); } function getConvexQuadArea(quad) { return getTriangleArea( quad[0], quad[1], quad[2]) + getTriangleArea( quad[0], quad[2], quad[3]); } function getTileSizeInMeters( latitude, zoom) { return EARTH_CIRCUMFERENCE_IN_METERS * Math.cos(latitude / 180 * Math.PI) / Math.pow(2, zoom); } function getPositionFromLocal(localXY) { var metersPerDegreeLongitude = METERS_PER_DEGREE_LATITUDE * Math.cos(APP.position.latitude / 180 * Math.PI); return { longitude: APP.position.longitude + localXY[0]/metersPerDegreeLongitude, latitude: APP.position.latitude - localXY[1]/METERS_PER_DEGREE_LATITUDE }; } function getTilePositionFromLocal(localXY, zoom) { var pos = getPositionFromLocal(localXY); return [long2tile(pos.longitude, zoom), lat2tile(pos.latitude, zoom)]; } //all four were taken from http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames function long2tile(lon,zoom) { return (lon+180)/360*Math.pow(2,zoom); } function lat2tile(lat,zoom) { return (1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom); } function tile2lon(x,z) { return (x/Math.pow(2,z)*360-180); } function tile2lat(y,z) { var n = Math.PI-2*Math.PI*y/Math.pow(2,z); return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)))); } function len2(a) { return Math.sqrt( a[0]*a[0] + a[1]*a[1]);} function dot2(a,b) { return a[0]*b[0] + a[1]*b[1];} function sub2(a,b) { return [a[0]-b[0], a[1]-b[1]];} function add2(a,b) { return [a[0]+b[0], a[1]+b[1]];} function mul2scalar(a,f) { return [a[0]*f, a[1]*f];} function norm2(a) { var l = len2(a); return [a[0]/l, a[1]/l]; } function dot3(a,b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];} function sub3(a,b) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]];} function add3(a,b) { return [a[0]+b[0], a[1]+b[1], a[2]+b[2]];} function add3scalar(a,f) { return [a[0]+f, a[1]+f, a[2]+f];} function mul3scalar(a,f) { return [a[0]*f, a[1]*f, a[2]*f];} function len3(a) { return Math.sqrt( a[0]*a[0] + a[1]*a[1] + a[2]*a[2]);} function squaredLength(a) { return a[0]*a[0] + a[1]*a[1] + a[2]*a[2];} function norm3(a) { var l = len3(a); return [a[0]/l, a[1]/l, a[2]/l]; } function dist3(a,b){ return len3(sub3(a,b));} function equal3(a, b) { return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];} var render = { getViewQuad: function() { return getViewQuad( this.viewProjMatrix.data, (this.fogDistance + this.fogBlurDistance), this.viewDirOnMap); }, start: function() { // disable effects if they rely on WebGL extensions // that the current hardware does not support if (!GL.depthTextureExtension) { console.log('[WARN] effects "shadows" and "outlines" disabled in OSMBuildings, because your GPU does not support WEBGL_depth_texture'); //both effects rely on depth textures delete render.effects.shadows; delete render.effects.outlines; } APP.on('change', this._onChange = this.onChange.bind(this)); APP.on('resize', this._onResize = this.onResize.bind(this)); this.onResize(); //initialize view and projection matrix, fog distance, etc. GL.cullFace(GL.BACK); GL.enable(GL.CULL_FACE); GL.enable(GL.DEPTH_TEST); render.Picking.init(); // renders only on demand render.sky = new render.SkyWall(); render.Buildings.init(); render.Basemap.init(); render.Overlay.init(); render.AmbientMap.init(); render.OutlineMap.init(); render.blurredAmbientMap = new render.Blur(); render.blurredOutlineMap = new render.Blur(); //render.HudRect.init(); //render.NormalMap.init(); render.MapShadows.init(); if (render.effects.shadows || render.effects.outlines) { render.cameraGBuffer = new render.DepthFogNormalMap(); } if (render.effects.shadows) { render.sunGBuffer = new render.DepthFogNormalMap(); render.sunGBuffer.framebufferSize = [SHADOW_DEPTH_MAP_SIZE, SHADOW_DEPTH_MAP_SIZE]; } //var quad = new mesh.DebugQuad(); //quad.updateGeometry( [-100, -100, 1], [100, -100, 1], [100, 100, 1], [-100, 100, 1]); //data.Index.add(quad); requestAnimationFrame( this.renderFrame.bind(this)); }, renderFrame: function() { if (GL === undefined) { return; } Filter.nextTick(); requestAnimationFrame( this.renderFrame.bind(this)); this.onChange(); GL.clearColor(this.fogColor[0], this.fogColor[1], this.fogColor[2], 0.0); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); if (APP.zoom < APP.minZoom || APP.zoom > APP.maxZoom) { return; } var viewTrapezoid = this.getViewQuad(); /* quad.updateGeometry([viewTrapezoid[0][0], viewTrapezoid[0][1], 1.0], [viewTrapezoid[1][0], viewTrapezoid[1][1], 1.0], [viewTrapezoid[2][0], viewTrapezoid[2][1], 1.0], [viewTrapezoid[3][0], viewTrapezoid[3][1], 1.0]);*/ Sun.updateView(viewTrapezoid); render.sky.updateGeometry(viewTrapezoid); var viewSize = [APP.width, APP.height]; if (!render.effects.shadows) { render.Buildings.render(); render.Basemap.render(); if (render.effects.outlines) { render.cameraGBuffer.render(this.viewMatrix, this.projMatrix, viewSize, true); render.Picking.render(viewSize); render.OutlineMap.render( render.cameraGBuffer.getDepthTexture(), render.cameraGBuffer.getFogNormalTexture(), render.Picking.framebuffer.renderTexture, viewSize, 1.0); render.blurredOutlineMap.render(render.OutlineMap.framebuffer.renderTexture, viewSize); } GL.enable(GL.BLEND); if (render.effects.outlines) { GL.blendFuncSeparate(GL.ZERO, GL.SRC_COLOR, GL.ZERO, GL.ONE); render.Overlay.render(render.blurredOutlineMap.framebuffer.renderTexture, viewSize); } GL.blendFuncSeparate(GL.ONE_MINUS_DST_ALPHA, GL.DST_ALPHA, GL.ONE, GL.ONE); GL.disable(GL.DEPTH_TEST); render.sky.render(); GL.disable(GL.BLEND); GL.enable(GL.DEPTH_TEST); } else { render.cameraGBuffer.render(this.viewMatrix, this.projMatrix, viewSize, true); render.sunGBuffer.render(Sun.viewMatrix, Sun.projMatrix); render.AmbientMap.render(render.cameraGBuffer.getDepthTexture(), render.cameraGBuffer.getFogNormalTexture(), viewSize, 2.0); render.blurredAmbientMap.render(render.AmbientMap.framebuffer.renderTexture, viewSize); render.Buildings.render(render.sunGBuffer.framebuffer, 0.5); render.Basemap.render(); if (render.effects.outlines) { render.Picking.render(viewSize); render.OutlineMap.render( render.cameraGBuffer.getDepthTexture(), render.cameraGBuffer.getFogNormalTexture(), render.Picking.framebuffer.renderTexture, viewSize, 1.0 ); render.blurredOutlineMap.render(render.OutlineMap.framebuffer.renderTexture, viewSize); } GL.enable(GL.BLEND); { // multiply DEST_COLOR by SRC_COLOR, keep SRC alpha // this aplies the shadow and SSAO effects (which selectively darken the scene) // while keeping the alpha channel (that corresponds to how much the // geometry should be blurred into the background in the next step) intact GL.blendFuncSeparate(GL.ZERO, GL.SRC_COLOR, GL.ZERO, GL.ONE); if (render.effects.outlines) { render.Overlay.render(render.blurredOutlineMap.framebuffer.renderTexture, viewSize); } render.MapShadows.render(Sun, render.sunGBuffer.framebuffer, 0.5); render.Overlay.render( render.blurredAmbientMap.framebuffer.renderTexture, viewSize); // linear interpolation between the colors of the current framebuffer // ( =building geometries) and of the sky. The interpolation factor // is the geometry alpha value, which contains the 'foggyness' of each pixel // the alpha interpolation functions is set to GL.ONE for both operands // to ensure that the alpha channel will become 1.0 for each pixel after this // operation, and thus the whole canvas is not rendered partially transparently // over its background. GL.blendFuncSeparate(GL.ONE_MINUS_DST_ALPHA, GL.DST_ALPHA, GL.ONE, GL.ONE); GL.disable(GL.DEPTH_TEST); render.sky.render(); GL.enable(GL.DEPTH_TEST); } GL.disable(GL.BLEND); //render.HudRect.render( render.sunGBuffer.getFogNormalTexture(), config ); } if (this.screenshotCallback) { this.screenshotCallback(GL.canvas.toDataURL()); this.screenshotCallback = null; } }, onChange: function() { var scale = 1.3567 * Math.pow(2, APP.zoom-17), width = APP.width, height = APP.height, refHeight = 1024, refVFOV = 45; GL.viewport(0, 0, width, height); this.viewMatrix = new GLX.Matrix() .rotateZ(APP.rotation) .rotateX(APP.tilt) .translate(0, 8/scale, 0) // corrective offset to match Leaflet's coordinate system (value was determined empirically) .translate(0, 0, -1220/scale); //move away to simulate zoom; -1220 scales APP tiles to ~256px this.viewDirOnMap = [ Math.sin(APP.rotation / 180* Math.PI), -Math.cos(APP.rotation / 180* Math.PI)]; // First, we need to determine the field-of-view so that our map scale does // not change when the viewport size changes. The map scale is given by the // 'refFOV' (e.g. 45°) at a WebGL viewport height of 'refHeight' pixels. // Since our viewport will not usually be 1024 pixels high, we'll need to // find the FOV that corresponds to our viewport height. // The half viewport height and half FOV form a leg and the opposite angle // of a right triangle (see sketch below). Since the relation between the // two is non-linear, we cannot simply scale the reference FOV by the ratio // of reference height to actual height to get the desired FOV. // But be can use the reference height and reference FOV to determine the // virtual distance to the camera and then use that virtual distance to // compute the FOV corresponding to the actual height. // // ____/| // ____/ | // ____/ | refHeight/2 // ____/ \ | // /refFOV/2| | // ----------------------| // "virtual distance" var virtualDistance = refHeight/ (2 * Math.tan( (refVFOV/2) / 180 * Math.PI)); var verticalFOV = 2* Math.atan((height/2.0)/virtualDistance) / Math.PI * 180; // OSMBuildings' perspective camera is ... special: The reference point for // camera movement, rotation and zoom is at the screen center (as usual). // But the center of projection is not at the screen center as well but at // the bottom center of the screen. This projection was chosen for artistic // reasons so that when the map is seen from straight above, vertical building // walls would not be seen to face towards the screen center but would // uniformly face downward on the screen. // To achieve this projection, we need to // 1. shift the whole geometry up half a screen (so that the desired // center of projection aligns with the view center) *in world coordinates*. // 2. perform the actual perspective projection (and flip the y coordinate for // internal reasons). // 3. shift the geometry back down half a screen now *in screen coordinates* this.projMatrix = new GLX.Matrix() .translate(0, -height/(2.0*scale), 0) // 0, APP y offset to neutralize camera y offset, .scale(1, -1, 1) // flip Y .multiply(new GLX.Matrix.Perspective(verticalFOV, width/height, 1, 7500)) .translate(0, -1, 0); // camera y offset this.viewProjMatrix = new GLX.Matrix(GLX.Matrix.multiply(this.viewMatrix, this.projMatrix)); //need to store this as a reference point to determine fog distance this.lowerLeftOnMap = getIntersectionWithXYPlane(-1, -1, GLX.Matrix.invert(this.viewProjMatrix.data)); if (this.lowerLeftOnMap === undefined) { return; } var lowerLeftDistanceToCenter = len2(this.lowerLeftOnMap); /* fogDistance: closest distance at which the fog affects the geometry */ this.fogDistance = Math.max(3000, lowerLeftDistanceToCenter); /* fogBlurDistance: closest distance *beyond* fogDistance at which everything is * completely enclosed in fog. */ this.fogBlurDistance = 500; }, onResize: function() { GL.canvas.width = APP.width; GL.canvas.height = APP.height; this.onChange(); }, destroy: function() { APP.off('change', this._onChange); APP.off('resize', this._onResize); render.Picking.destroy(); render.sky.destroy(); render.Buildings.destroy(); render.Basemap.destroy(); if (render.cameraGBuffer) { render.cameraGBuffer.destroy(); } if (render.sunGBuffer) { render.sunGBuffer.destroy(); } render.AmbientMap.destroy(); render.blurredAmbientMap.destroy(); render.blurredOutlineMap.destroy(); } }; // TODO: perhaps render only clicked area render.Picking = { idMapping: [null], viewportSize: 512, init: function() { this.shader = new GLX.Shader({ vertexShader: Shaders.picking.vertex, fragmentShader: Shaders.picking.fragment, shaderName: 'picking shader', attributes: ['aPosition', 'aId', 'aFilter'], uniforms: [ 'uModelMatrix', 'uMatrix', 'uFogRadius', 'uTime' ] }); this.framebuffer = new GLX.Framebuffer(this.viewportSize, this.viewportSize); }, render: function(framebufferSize) { var shader = this.shader, framebuffer = this.framebuffer; framebuffer.setSize(framebufferSize[0], framebufferSize[1]); shader.enable(); framebuffer.enable(); GL.viewport(0, 0, framebufferSize[0], framebufferSize[1]); GL.clearColor(0, 0, 0, 1); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); shader.setUniforms([ ['uFogRadius', '1f', render.fogDistance], ['uTime', '1f', Filter.getTime()] ]); var dataItems = data.Index.items, item, modelMatrix; for (var i = 0, il = dataItems.length; i item.maxZoom) { continue; } if (!(modelMatrix = item.getMatrix())) { continue; } shader.setUniformMatrices([ ['uModelMatrix', '4fv', modelMatrix.data], ['uMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, render.viewProjMatrix)] ]); shader.bindBuffer(item.vertexBuffer, 'aPosition'); shader.bindBuffer(item.idBuffer, 'aId'); shader.bindBuffer(item.filterBuffer, 'aFilter'); GL.drawArrays(GL.TRIANGLES, 0, item.vertexBuffer.numItems); } this.shader.disable(); this.framebuffer.disable(); GL.viewport(0, 0, APP.width, APP.height); }, // TODO: throttle calls getTarget: function(x, y, callback) { requestAnimationFrame(function() { this.render( [this.viewportSize, this.viewportSize] ); x = x/APP.width *this.viewportSize <<0; y = y/APP.height*this.viewportSize <<0; this.framebuffer.enable(); var imageData = this.framebuffer.getPixel(x, this.viewportSize - 1 - y); this.framebuffer.disable(); if (imageData === undefined) { callback(undefined); return; } var color = imageData[0] | (imageData[1]<<8) | (imageData[2]<<16); callback(this.idMapping[color]); }.bind(this)); }, idToColor: function(id) { var index = this.idMapping.indexOf(id); if (index === -1) { this.idMapping.push(id); index = this.idMapping.length-1; } return [ ( index & 0xff) / 255, ((index >> 8) & 0xff) / 255, ((index >> 16) & 0xff) / 255 ]; }, destroy: function() {} }; var Sun = { setDate: function(date) { var pos = suncalc(date, APP.position.latitude, APP.position.longitude); this.direction = [ -Math.sin(pos.azimuth) * Math.cos(pos.altitude), Math.cos(pos.azimuth) * Math.cos(pos.altitude), Math.sin(pos.altitude) ]; var rotationInDeg = pos.azimuth / (Math.PI/180); var tiltInDeg = 90 - pos.altitude / (Math.PI/180); this.viewMatrix = new GLX.Matrix() .rotateZ(rotationInDeg) .rotateX(tiltInDeg) .translate(0, 0, -5000) .scale(1, -1, 1); // flip Y }, updateView: function(coveredGroundVertices) { // TODO: could parts be pre-calculated? this.projMatrix = getCoveringOrthoProjection( substituteZCoordinate(coveredGroundVertices, 0.0).concat(substituteZCoordinate(coveredGroundVertices,SHADOW_MAP_MAX_BUILDING_HEIGHT)), this.viewMatrix, 1000, 7500 ); this.viewProjMatrix = new GLX.Matrix(GLX.Matrix.multiply(this.viewMatrix, this.projMatrix)); } }; render.SkyWall = function() { this.v1 = this.v2 = this.v3 = this.v4 = [false, false, false]; this.updateGeometry( [[0,0,0], [0,0,0], [0,0,0], [0,0,0]]); this.shader = new GLX.Shader({ vertexShader: Shaders.skywall.vertex, fragmentShader: Shaders.skywall.fragment, shaderName: 'sky wall shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uAbsoluteHeight', 'uMatrix', 'uTexIndex', 'uFogColor'] }); this.floorShader = new GLX.Shader({ vertexShader: Shaders.flatColor.vertex, fragmentShader: Shaders.flatColor.fragment, attributes: ['aPosition'], uniforms: ['uColor', 'uMatrix'] }); Activity.setBusy(); var url = APP.baseURL + '/skydome.jpg'; this.texture = new GLX.texture.Image().load(url, function(image) { Activity.setIdle(); if (image) { this.isReady = true; } }.bind(this)); }; render.SkyWall.prototype.updateGeometry = function(viewTrapezoid) { var v1 = [viewTrapezoid[3][0], viewTrapezoid[3][1], 0.0]; var v2 = [viewTrapezoid[2][0], viewTrapezoid[2][1], 0.0]; var v3 = [viewTrapezoid[2][0], viewTrapezoid[2][1], SKYWALL_HEIGHT]; var v4 = [viewTrapezoid[3][0], viewTrapezoid[3][1], SKYWALL_HEIGHT]; if ( equal3(v1, this.v1) && equal3(v2, this.v2) && equal3(v3, this.v3) && equal3(v4, this.v4)) return; //still up-to-date this.v1 = v1; this.v2 = v2; this.v3 = v3; this.v4 = v4; if (this.vertexBuffer) this.vertexBuffer.destroy(); var vertices = [].concat(v1, v2, v3, v1, v3, v4); this.vertexBuffer = new GLX.Buffer(3, new Float32Array(vertices)); if (this.texCoordBuffer) this.texCoordBuffer.destroy(); var inverse = GLX.Matrix.invert(render.viewProjMatrix.data); var vBottomCenter = getIntersectionWithXYPlane(0, -1, inverse); var vLeftDir = norm2(sub2( v1, vBottomCenter)); var vRightDir =norm2(sub2( v2, vBottomCenter)); var vLeftArc = Math.atan2(vLeftDir[1], vLeftDir[0])/ (2*Math.PI); var vRightArc= Math.atan2(vRightDir[1], vRightDir[0])/ (2*Math.PI); if (vLeftArc > vRightArc) vRightArc +=1; //console.log(vLeftArc, vRightArc); // var visibleSkyDiameterFraction = Math.asin(dot2( vLeftDir, vRightDir))/ (2*Math.PI); var tcLeft = vLeftArc;//APP.rotation/360.0; var tcRight =vRightArc;//APP.rotation/360.0 + visibleSkyDiameterFraction*3; this.texCoordBuffer = new GLX.Buffer(2, new Float32Array( [tcLeft, 1, tcRight, 1, tcRight, 0, tcLeft, 1, tcRight, 0, tcLeft, 0])); v1 = [viewTrapezoid[0][0], viewTrapezoid[0][1], 1.0]; v2 = [viewTrapezoid[1][0], viewTrapezoid[1][1], 1.0]; v3 = [viewTrapezoid[2][0], viewTrapezoid[2][1], 1.0]; v4 = [viewTrapezoid[3][0], viewTrapezoid[3][1], 1.0]; if (this.floorVertexBuffer) this.floorVertexBuffer.destroy(); this.floorVertexBuffer = new GLX.Buffer(3, new Float32Array( [].concat( v1, v2, v3, v4))); }; render.SkyWall.prototype.render = function() { if (!this.isReady) { return; } var fogColor = render.fogColor, shader = this.shader; shader.enable(); shader.setUniforms([ ['uFogColor', '3fv', fogColor], ['uAbsoluteHeight', '1f', SKYWALL_HEIGHT*10.0] ]); shader.setUniformMatrix('uMatrix', '4fv', render.viewProjMatrix.data); shader.bindBuffer( this.vertexBuffer, 'aPosition'); shader.bindBuffer( this.texCoordBuffer, 'aTexCoord'); shader.bindTexture('uTexIndex', 0, this.texture); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); shader.disable(); this.floorShader.enable(); this.floorShader.setUniform('uColor', '4fv', fogColor.concat([1.0])); this.floorShader.setUniformMatrix('uMatrix', '4fv', render.viewProjMatrix.data); this.floorShader.bindBuffer(this.floorVertexBuffer, 'aPosition'); GL.drawArrays(GL.TRIANGLE_FAN, 0, this.floorVertexBuffer.numItems); this.floorShader.disable(); }; render.SkyWall.prototype.destroy = function() { this.texture.destroy(); }; render.Buildings = { init: function() { this.shader = !render.effects.shadows ? new GLX.Shader({ vertexShader: Shaders.buildings.vertex, fragmentShader: Shaders.buildings.fragment, shaderName: 'building shader', attributes: ['aPosition', 'aTexCoord', 'aColor', 'aFilter', 'aNormal', 'aId', 'aHeight'], uniforms: [ 'uModelMatrix', 'uViewDirOnMap', 'uMatrix', 'uNormalTransform', 'uLightColor', 'uLightDirection', 'uLowerEdgePoint', 'uFogDistance', 'uFogBlurDistance', 'uHighlightColor', 'uHighlightId', 'uTime', 'uWallTexIndex' ] }) : new GLX.Shader({ vertexShader: Shaders['buildings.shadows'].vertex, fragmentShader: Shaders['buildings.shadows'].fragment, shaderName: 'quality building shader', attributes: ['aPosition', 'aTexCoord', 'aColor', 'aFilter', 'aNormal', 'aId', 'aHeight'], uniforms: [ 'uFogDistance', 'uFogBlurDistance', 'uHighlightColor', 'uHighlightId', 'uLightColor', 'uLightDirection', 'uLowerEdgePoint', 'uMatrix', 'uModelMatrix', 'uSunMatrix', 'uShadowTexIndex', 'uShadowTexDimensions', 'uTime', 'uViewDirOnMap', 'uWallTexIndex' ] }); this.wallTexture = new GLX.texture.Image(); this.wallTexture.color( [1,1,1]); this.wallTexture.load( BUILDING_TEXTURE); }, render: function(depthFramebuffer) { var shader = this.shader; shader.enable(); if (this.showBackfaces) { GL.disable(GL.CULL_FACE); } shader.setUniforms([ ['uFogDistance', '1f', render.fogDistance], ['uFogBlurDistance', '1f', render.fogBlurDistance], ['uHighlightColor', '3fv', this.highlightColor || [0, 0, 0]], ['uHighlightId', '3fv', this.highlightId || [0, 0, 0]], ['uLightColor', '3fv', [0.5, 0.5, 0.5]], ['uLightDirection', '3fv', Sun.direction], ['uLowerEdgePoint', '2fv', render.lowerLeftOnMap], ['uTime', '1f', Filter.getTime()], ['uViewDirOnMap', '2fv', render.viewDirOnMap] ]); if (!render.effects.shadows) { shader.setUniformMatrix('uNormalTransform', '3fv', GLX.Matrix.identity3().data); } shader.bindTexture('uWallTexIndex', 0, this.wallTexture); if (depthFramebuffer) { shader.setUniform('uShadowTexDimensions', '2fv', [depthFramebuffer.width, depthFramebuffer.height]); shader.bindTexture('uShadowTexIndex', 1, depthFramebuffer.depthTexture); } var dataItems = data.Index.items, item, modelMatrix; for (var i = 0, il = dataItems.length; i < il; i++) { // no visibility check needed, Grid.purge() is taking care item = dataItems[i]; if (APP.zoom < item.minZoom || APP.zoom > item.maxZoom || !(modelMatrix = item.getMatrix())) { continue; } shader.setUniformMatrices([ ['uModelMatrix', '4fv', modelMatrix.data], ['uMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, render.viewProjMatrix)] ]); if (render.effects.shadows) { shader.setUniformMatrix('uSunMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, Sun.viewProjMatrix)); } shader.bindBuffer(item.vertexBuffer, 'aPosition'); shader.bindBuffer(item.texCoordBuffer, 'aTexCoord'); shader.bindBuffer(item.normalBuffer, 'aNormal'); shader.bindBuffer(item.colorBuffer, 'aColor'); shader.bindBuffer(item.idBuffer, 'aId'); shader.bindBuffer(item.filterBuffer, 'aFilter'); shader.bindBuffer(item.heightBuffer, 'aHeight'); GL.drawArrays(GL.TRIANGLES, 0, item.vertexBuffer.numItems); } if (this.showBackfaces) { GL.enable(GL.CULL_FACE); } shader.disable(); }, destroy: function() {} }; /* This object renders the shadow for the map layer. It only renders the shadow, * not the map itself. The intended use for this class is as a blended overlay * so that the map can be rendered independently from the shadows cast on it. */ render.MapShadows = { init: function() { this.shader = new GLX.Shader({ vertexShader: Shaders['basemap.shadows'].vertex, fragmentShader: Shaders['basemap.shadows'].fragment, shaderName: 'map shadows shader', attributes: ['aPosition', 'aNormal'], uniforms: [ 'uModelMatrix', 'uViewDirOnMap', 'uMatrix', 'uDirToSun', 'uLowerEdgePoint', 'uFogDistance', 'uFogBlurDistance', 'uShadowTexDimensions', 'uShadowStrength', 'uShadowTexIndex', 'uSunMatrix', ] }); this.mapPlane = new mesh.MapPlane(); }, render: function(Sun, depthFramebuffer, shadowStrength) { var shader = this.shader; shader.enable(); if (this.showBackfaces) { GL.disable(GL.CULL_FACE); } shader.setUniforms([ ['uDirToSun', '3fv', Sun.direction], ['uViewDirOnMap', '2fv', render.viewDirOnMap], ['uLowerEdgePoint', '2fv', render.lowerLeftOnMap], ['uFogDistance', '1f', render.fogDistance], ['uFogBlurDistance', '1f', render.fogBlurDistance], ['uShadowTexDimensions', '2fv', [depthFramebuffer.width, depthFramebuffer.height] ], ['uShadowStrength', '1f', shadowStrength] ]); shader.bindTexture('uShadowTexIndex', 0, depthFramebuffer.depthTexture); var item = this.mapPlane; if (APP.zoom < item.minZoom || APP.zoom > item.maxZoom) { return; } var modelMatrix; if (!(modelMatrix = item.getMatrix())) { return; } shader.setUniformMatrices([ ['uModelMatrix', '4fv', modelMatrix.data], ['uMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, render.viewProjMatrix)], ['uSunMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, Sun.viewProjMatrix)] ]); shader.bindBuffer(item.vertexBuffer, 'aPosition'); shader.bindBuffer(item.normalBuffer, 'aNormal'); GL.drawArrays(GL.TRIANGLES, 0, item.vertexBuffer.numItems); if (this.showBackfaces) { GL.enable(GL.CULL_FACE); } shader.disable(); }, destroy: function() {} }; render.Basemap = { init: function() { this.shader = new GLX.Shader({ vertexShader: Shaders.basemap.vertex, fragmentShader: Shaders.basemap.fragment, shaderName: 'basemap shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uModelMatrix', 'uMatrix', 'uTexIndex', 'uFogDistance', 'uFogBlurDistance', 'uLowerEdgePoint', 'uViewDirOnMap'] }); }, render: function() { var layer = APP.basemapGrid; if (!layer) { return; } if (APP.zoom < layer.minZoom || APP.zoom > layer.maxZoom) { return; } var shader = this.shader, tile, zoom = Math.round(APP.zoom); shader.enable(); shader.setUniforms([ ['uFogDistance', '1f', render.fogDistance], ['uFogBlurDistance', '1f', render.fogBlurDistance], ['uViewDirOnMap', '2fv', render.viewDirOnMap], ['uLowerEdgePoint', '2fv', render.lowerLeftOnMap] ]); for (var key in layer.visibleTiles) { tile = layer.tiles[key]; if (tile && tile.isReady) { this.renderTile(tile, shader); continue; } var parent = [tile.x/2<<0, tile.y/2<<0, zoom-1].join(','); if (layer.tiles[parent] && layer.tiles[parent].isReady) { // TODO: there will be overlap with adjacent tiles or parents of adjacent tiles! this.renderTile(layer.tiles[ parent ], shader); continue; } var children = [ [tile.x*2, tile.y*2, tile.zoom+1].join(','), [tile.x*2+1, tile.y*2, tile.zoom+1].join(','), [tile.x*2, tile.y*2+1, tile.zoom+1].join(','), [tile.x*2+1, tile.y*2+1, tile.zoom+1].join(',') ]; for (var i = 0; i < 4; i++) { if (layer.tiles[ children[i] ] && layer.tiles[ children[i] ].isReady) { this.renderTile(layer.tiles[ children[i] ], shader); } } } shader.disable(); }, renderTile: function(tile, shader) { var metersPerDegreeLongitude = METERS_PER_DEGREE_LATITUDE * Math.cos(APP.position.latitude / 180 * Math.PI); var modelMatrix = new GLX.Matrix(); modelMatrix.translate( (tile.longitude- APP.position.longitude)* metersPerDegreeLongitude, -(tile.latitude - APP.position.latitude) * METERS_PER_DEGREE_LATITUDE, 0); GL.enable(GL.POLYGON_OFFSET_FILL); GL.polygonOffset(MAX_USED_ZOOM_LEVEL - tile.zoom, MAX_USED_ZOOM_LEVEL - tile.zoom); shader.setUniforms([ ['uViewDirOnMap', '2fv', render.viewDirOnMap], ['uLowerEdgePoint', '2fv', render.lowerLeftOnMap] ]); shader.setUniformMatrices([ ['uModelMatrix', '4fv', modelMatrix.data], ['uMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, render.viewProjMatrix)] ]); shader.bindBuffer(tile.vertexBuffer, 'aPosition'); shader.bindBuffer(tile.texCoordBuffer,'aTexCoord'); shader.bindTexture('uTexIndex', 0, tile.texture); GL.drawArrays(GL.TRIANGLE_STRIP, 0, tile.vertexBuffer.numItems); GL.disable(GL.POLYGON_OFFSET_FILL); }, destroy: function() {} }; /* 'HudRect' renders a textured rectangle to the top-right quarter of the viewport. The intended use is visualize render-to-texture effects during development. */ render.HudRect = { init: function() { var geometry = this.createGeometry(); this.vertexBuffer = new GLX.Buffer(3, new Float32Array(geometry.vertices)); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array(geometry.texCoords)); this.shader = new GLX.Shader({ vertexShader: Shaders.texture.vertex, fragmentShader: Shaders.texture.fragment, shaderName: 'HUD rectangle shader', attributes: ['aPosition', 'aTexCoord'], uniforms: [ 'uMatrix', 'uTexIndex'] }); }, createGeometry: function() { var vertices = [], texCoords= []; vertices.push(0, 0, 1E-5, 1, 0, 1E-5, 1, 1, 1E-5); vertices.push(0, 0, 1E-5, 1, 1, 1E-5, 0, 1, 1E-5); texCoords.push(0.5,0.5, 1.0,0.5, 1.0,1.0); texCoords.push(0.5,0.5, 1.0,1.0, 0.5,1.0); return { vertices: vertices , texCoords: texCoords }; }, render: function(texture) { var shader = this.shader; shader.enable(); GL.uniformMatrix4fv(shader.uniforms.uMatrix, false, GLX.Matrix.identity().data); this.vertexBuffer.enable(); GL.vertexAttribPointer(shader.attributes.aPosition, this.vertexBuffer.itemSize, GL.FLOAT, false, 0, 0); this.texCoordBuffer.enable(); GL.vertexAttribPointer(shader.attributes.aTexCoord, this.texCoordBuffer.itemSize, GL.FLOAT, false, 0, 0); texture.enable(0); GL.uniform1i(shader.uniforms.uTexIndex, 0); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); shader.disable(); }, destroy: function() {} }; /* 'DepthFogNormalMap' renders the depth buffer and the scene's camera-space normals and fog intensities into textures. Depth is stored as a 24bit depth texture using the WEBGL_depth_texture extension, and normals and fog intensities are stored as the 'rgb' and 'a' of a shared 32bit texture. Note that there is no dedicated shader to create the depth texture. Rather, the depth buffer used by the GPU in depth testing while rendering the normals and fog intensities is itself a texture. */ render.DepthFogNormalMap = function() { this.shader = new GLX.Shader({ vertexShader: Shaders.fogNormal.vertex, fragmentShader: Shaders.fogNormal.fragment, shaderName: 'fog/normal shader', attributes: ['aPosition', 'aFilter', 'aNormal'], uniforms: ['uMatrix', 'uModelMatrix', 'uNormalMatrix', 'uTime', 'uFogDistance', 'uFogBlurDistance', 'uViewDirOnMap', 'uLowerEdgePoint'] }); this.framebuffer = new GLX.Framebuffer(128, 128, /*depthTexture=*/true); //dummy sizes, will be resized dynamically this.mapPlane = new mesh.MapPlane(); }; render.DepthFogNormalMap.prototype.getDepthTexture = function() { return this.framebuffer.depthTexture; }; render.DepthFogNormalMap.prototype.getFogNormalTexture = function() { return this.framebuffer.renderTexture; }; render.DepthFogNormalMap.prototype.render = function(viewMatrix, projMatrix, framebufferSize, isPerspective) { var shader = this.shader, framebuffer = this.framebuffer, viewProjMatrix = new GLX.Matrix(GLX.Matrix.multiply(viewMatrix,projMatrix)); framebufferSize = framebufferSize || this.framebufferSize; framebuffer.setSize( framebufferSize[0], framebufferSize[1] ); shader.enable(); framebuffer.enable(); GL.viewport(0, 0, framebufferSize[0], framebufferSize[1]); GL.clearColor(0.0, 0.0, 0.0, 1); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); var item, modelMatrix; shader.setUniform('uTime', '1f', Filter.getTime()); // render all actual data items, but also a dummy map plane // Note: SSAO on the map plane has been disabled temporarily var dataItems = data.Index.items.concat([this.mapPlane]); for (var i = 0; i < dataItems.length; i++) { item = dataItems[i]; if (APP.zoom < item.minZoom || APP.zoom > item.maxZoom) { continue; } if (!(modelMatrix = item.getMatrix())) { continue; } shader.setUniforms([ ['uViewDirOnMap', '2fv', render.viewDirOnMap], ['uLowerEdgePoint', '2fv', render.lowerLeftOnMap], ['uFogDistance', '1f', render.fogDistance], ['uFogBlurDistance', '1f', render.fogBlurDistance] ]); shader.setUniformMatrices([ ['uMatrix', '4fv', GLX.Matrix.multiply(modelMatrix, viewProjMatrix)], ['uModelMatrix', '4fv', modelMatrix.data], ['uNormalMatrix', '3fv', GLX.Matrix.transpose3(GLX.Matrix.invert3(GLX.Matrix.multiply(modelMatrix, viewMatrix)))] ]); shader.bindBuffer(item.vertexBuffer, 'aPosition'); shader.bindBuffer(item.normalBuffer, 'aNormal'); shader.bindBuffer(item.filterBuffer, 'aFilter'); GL.drawArrays(GL.TRIANGLES, 0, item.vertexBuffer.numItems); } shader.disable(); framebuffer.disable(); GL.viewport(0, 0, APP.width, APP.height); }; render.DepthFogNormalMap.prototype.destroy = function() {}; render.AmbientMap = { init: function() { this.shader = new GLX.Shader({ vertexShader: Shaders.ambientFromDepth.vertex, fragmentShader: Shaders.ambientFromDepth.fragment, shaderName: 'SSAO shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uInverseTexSize', 'uNearPlane', 'uFarPlane', 'uDepthTexIndex', 'uFogTexIndex', 'uEffectStrength'] }); this.framebuffer = new GLX.Framebuffer(128, 128); //dummy value, size will be set dynamically this.vertexBuffer = new GLX.Buffer(3, new Float32Array([ -1, -1, 1E-5, 1, -1, 1E-5, 1, 1, 1E-5, -1, -1, 1E-5, 1, 1, 1E-5, -1, 1, 1E-5 ])); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array([ 0,0, 1,0, 1,1, 0,0, 1,1, 0,1 ])); }, render: function(depthTexture, fogTexture, framebufferSize, effectStrength) { var shader = this.shader, framebuffer = this.framebuffer; if (effectStrength === undefined) { effectStrength = 1.0; } framebuffer.setSize( framebufferSize[0], framebufferSize[1] ); GL.viewport(0, 0, framebufferSize[0], framebufferSize[1]); shader.enable(); framebuffer.enable(); GL.clearColor(1.0, 0.0, 0.0, 1); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); shader.setUniforms([ ['uInverseTexSize', '2fv', [1/framebufferSize[0], 1/framebufferSize[1]]], ['uEffectStrength', '1f', effectStrength], ['uNearPlane', '1f', 1.0], //FIXME: use actual near and far planes of the projection matrix ['uFarPlane', '1f', 7500.0] ]); shader.bindBuffer(this.vertexBuffer, 'aPosition'); shader.bindBuffer(this.texCoordBuffer, 'aTexCoord'); shader.bindTexture('uDepthTexIndex', 0, depthTexture); shader.bindTexture('uFogTexIndex', 1, fogTexture); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); shader.disable(); framebuffer.disable(); GL.viewport(0, 0, APP.width, APP.height); }, destroy: function() {} }; /* 'Overlay' renders part of a texture over the whole viewport. The intended use is for compositing of screen-space effects. */ render.Overlay = { init: function() { var geometry = this.createGeometry(); this.vertexBuffer = new GLX.Buffer(3, new Float32Array(geometry.vertices)); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array(geometry.texCoords)); this.shader = new GLX.Shader({ vertexShader: Shaders.texture.vertex, fragmentShader: Shaders.texture.fragment, shaderName: 'overlay texture shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uMatrix', 'uTexIndex'] }); }, createGeometry: function() { var vertices = [], texCoords= []; vertices.push(-1,-1, 1E-5, 1,-1, 1E-5, 1, 1, 1E-5); vertices.push(-1,-1, 1E-5, 1, 1, 1E-5, -1, 1, 1E-5); texCoords.push(0.0,0.0, 1.0,0.0, 1.0,1.0); texCoords.push(0.0,0.0, 1.0,1.0, 0.0,1.0); return { vertices: vertices , texCoords: texCoords }; }, render: function(texture, framebufferSize) { var shader = this.shader; shader.enable(); /* we are rendering an *overlay*, which is supposed to be rendered on top of the * scene no matter what its actual depth is. */ GL.disable(GL.DEPTH_TEST); shader.setUniformMatrix('uMatrix', '4fv', GLX.Matrix.identity().data); shader.bindBuffer(this.vertexBuffer, 'aPosition'); shader.bindBuffer(this.texCoordBuffer,'aTexCoord'); shader.bindTexture('uTexIndex', 0, texture); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); GL.enable(GL.DEPTH_TEST); shader.disable(); }, destroy: function() {} }; render.OutlineMap = { init: function() { this.shader = new GLX.Shader({ vertexShader: Shaders.outlineMap.vertex, fragmentShader: Shaders.outlineMap.fragment, shaderName: 'outline map shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uMatrix', 'uInverseTexSize', 'uNearPlane', 'uFarPlane', 'uDepthTexIndex', 'uFogNormalTexIndex', 'uIdTexIndex', 'uEffectStrength'] }); this.framebuffer = new GLX.Framebuffer(128, 128); //dummy value, size will be set dynamically this.vertexBuffer = new GLX.Buffer(3, new Float32Array([ -1, -1, 1E-5, 1, -1, 1E-5, 1, 1, 1E-5, -1, -1, 1E-5, 1, 1, 1E-5, -1, 1, 1E-5 ])); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array([ 0,0, 1,0, 1,1, 0,0, 1,1, 0,1 ])); }, render: function(depthTexture, fogNormalTexture, idTexture, framebufferSize, effectStrength) { var shader = this.shader, framebuffer = this.framebuffer; if (effectStrength === undefined) { effectStrength = 1.0; } framebuffer.setSize( framebufferSize[0], framebufferSize[1] ); GL.viewport(0, 0, framebufferSize[0], framebufferSize[1]); shader.enable(); framebuffer.enable(); GL.clearColor(1.0, 0.0, 0.0, 1); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); GL.uniformMatrix4fv(shader.uniforms.uMatrix, false, GLX.Matrix.identity().data); shader.setUniforms([ ['uInverseTexSize', '2fv', [1/framebufferSize[0], 1/framebufferSize[1]]], ['uEffectStrength', '1f', effectStrength], ['uNearPlane', '1f', 1.0], //FIXME: use actual near and far planes of the projection matrix ['uFarPlane', '1f', 7500.0] ]); shader.bindBuffer(this.vertexBuffer, 'aPosition'); shader.bindBuffer(this.texCoordBuffer, 'aTexCoord'); shader.bindTexture('uDepthTexIndex', 0, depthTexture); shader.bindTexture('uFogNormalTexIndex',1, fogNormalTexture); shader.bindTexture('uIdTexIndex', 2, idTexture); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); shader.disable(); framebuffer.disable(); GL.viewport(0, 0, APP.width, APP.height); }, destroy: function() {} }; render.Blur = function() { this.shader = new GLX.Shader({ vertexShader: Shaders.blur.vertex, fragmentShader: Shaders.blur.fragment, shaderName: 'blur shader', attributes: ['aPosition', 'aTexCoord'], uniforms: ['uInverseTexSize', 'uTexIndex'] }); this.framebuffer = new GLX.Framebuffer(128, 128); //dummy value, size will be set dynamically this.vertexBuffer = new GLX.Buffer(3, new Float32Array([ -1, -1, 1E-5, 1, -1, 1E-5, 1, 1, 1E-5, -1, -1, 1E-5, 1, 1, 1E-5, -1, 1, 1E-5 ])); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array([ 0,0, 1,0, 1,1, 0,0, 1,1, 0,1 ])); }; render.Blur.prototype.render = function(inputTexture, framebufferSize) { var shader = this.shader, framebuffer = this.framebuffer; framebuffer.setSize( framebufferSize[0], framebufferSize[1] ); GL.viewport(0, 0, framebufferSize[0], framebufferSize[1]); shader.enable(); framebuffer.enable(); GL.clearColor(1.0, 0.0, 0, 1); GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); shader.setUniform('uInverseTexSize', '2fv', [1/framebuffer.width, 1/framebuffer.height]); shader.bindBuffer(this.vertexBuffer, 'aPosition'); shader.bindBuffer(this.texCoordBuffer,'aTexCoord'); shader.bindTexture('uTexIndex', 0, inputTexture); GL.drawArrays(GL.TRIANGLES, 0, this.vertexBuffer.numItems); shader.disable(); framebuffer.disable(); GL.viewport(0, 0, APP.width, APP.height); }; render.Blur.prototype.destroy = function() { if (this.framebuffer) { this.framebuffer.destroy(); } }; var basemap = {}; basemap.Tile = function(x, y, zoom) { this.x = x; this.y = y; this.latitude = tile2lat(y, zoom); this.longitude= tile2lon(x, zoom); this.zoom = zoom; this.key = [x, y, zoom].join(','); // note: due to Mercator projection the tile width in meters is equal to tile height in meters. var size = getTileSizeInMeters(this.latitude, zoom); var vertices = [ size, size, 0, size, 0, 0, 0, size, 0, 0, 0, 0 ]; var texCoords = [ 1, 0, 1, 1, 0, 0, 0, 1 ]; this.vertexBuffer = new GLX.Buffer(3, new Float32Array(vertices)); this.texCoordBuffer = new GLX.Buffer(2, new Float32Array(texCoords)); }; basemap.Tile.prototype = { load: function(url) { Activity.setBusy(); this.texture = new GLX.texture.Image().load(url, function(image) { Activity.setIdle(); if (image) { this.isReady = true; /* The whole texture will be mapped to fit the whole tile exactly. So * don't attempt to wrap around the texture coordinates. */ GL.bindTexture(GL.TEXTURE_2D, this.texture.id); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); } }.bind(this)); }, destroy: function() { this.vertexBuffer.destroy(); this.texCoordBuffer.destroy(); if (this.texture) { this.texture.destroy(); } } }; }());