├── .gitignore ├── .jscs.json ├── Gruntfile.js ├── LICENSE.txt ├── README.md ├── bower.json ├── build ├── THREE.Terrain.js ├── THREE.Terrain.min.js ├── THREE.Terrain.min.js.map └── THREE.Terrain.min.map ├── demo ├── img │ ├── favicon.png │ ├── grass1.jpg │ ├── heightmap.png │ ├── sand1.jpg │ ├── screenshot1.jpg │ ├── screenshot2.jpg │ ├── sky1.jpg │ ├── snow1.jpg │ └── stone1.jpg ├── index.css ├── index.js └── libs │ ├── FirstPersonControls.js │ ├── dat.gui.min.js │ ├── stats.min.js │ ├── three.js │ └── three.min.js ├── index.html ├── package.json ├── roadmap.md ├── src ├── analysis.js ├── brownian.js ├── core.js ├── filters.js ├── gaussian.js ├── generators.js ├── images.js ├── influences.js ├── materials.js ├── noise.js ├── scatter.js ├── weightedBoxBlurGaussian.js └── worley.js └── statistics ├── README.md ├── images ├── favicon.png └── heightmap.png ├── index.html ├── scripts └── index.js └── styles └── index.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": { 3 | "allExcept": ["comments"] 4 | }, 5 | "disallowImplicitTypeConversion": [], 6 | "disallowKeywords": ["with"], 7 | "disallowKeywordsOnNewLine": [], 8 | "disallowMixedSpacesAndTabs": true, 9 | "disallowMultipleLineBreaks": true, 10 | "disallowNewlineBeforeBlockStatements": true, 11 | "disallowOperatorBeforeLineBreak": ["."], 12 | "disallowSpaceAfterObjectKeys": true, 13 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "~", "!"], 14 | "disallowSpaceBeforeBinaryOperators": [","], 15 | "disallowSpaceBeforeComma": true, 16 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 17 | "disallowSpaceBeforeSemicolon": true, 18 | "disallowSpacesInCallExpression": true, 19 | "disallowSpacesInFunctionDeclaration": { 20 | "beforeOpeningRoundBrace": true 21 | }, 22 | "disallowSpacesInNamedFunctionExpression": { 23 | "beforeOpeningRoundBrace": true 24 | }, 25 | "disallowSpacesInsideBrackets": { 26 | "allExcept": ["[", "]", "{", "}"] 27 | }, 28 | "disallowTrailingWhitespace": true, 29 | "disallowYodaConditions": true, 30 | "requireBlocksOnNewline": 1, 31 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 32 | "requireCapitalizedConstructors": true, 33 | "requireCommaBeforeLineBreak": true, 34 | "requireCurlyBraces": ["for", "while", "do", "try", "catch"], 35 | "requireKeywordsOnNewLine": ["else"], 36 | "requireLineBreakAfterVariableAssignment": true, 37 | "requireLineFeedAtFileEnd": true, 38 | "requireParenthesesAroundIIFE": true, 39 | "requireSemicolons": true, 40 | "requireSpaceAfterBinaryOperators": ["=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 41 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "case", "return", "try", "void", "with", "typeof"], 42 | "requireSpaceAfterLineComment": { 43 | "allExcept": ["#", "="] 44 | }, 45 | "requireSpaceBeforeBinaryOperators": ["=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 46 | "requireSpaceBeforeBlockStatements": true, 47 | "requireSpaceBeforeObjectValues": true, 48 | "requireSpaceBetweenArguments": true, 49 | "requireSpacesInConditionalExpression": { 50 | "afterTest": true, 51 | "beforeConsequent": true, 52 | "afterConsequent": true, 53 | "beforeAlternate": true 54 | }, 55 | "requireSpacesInForStatement": true, 56 | "requireSpacesInFunction": { 57 | "beforeOpeningCurlyBrace": true 58 | }, 59 | "requireSpacesInFunctionExpression": { 60 | "beforeOpeningCurlyBrace": true 61 | }, 62 | "validateQuoteMarks": { 63 | "escape": true, 64 | "mark": "'" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | var banner = '/**\n' + 3 | ' * THREE.Terrain.js <%= pkg.version %>-<%= grunt.template.today("yyyymmdd") %>\n' + 4 | ' *\n' + 5 | ' * @author <%= pkg.author %>\n' + 6 | ' * @license <%= pkg.license %>\n' + 7 | ' */\n'; 8 | grunt.initConfig({ 9 | pkg: grunt.file.readJSON('package.json'), 10 | concat: { 11 | options: { 12 | banner: banner + '\n', 13 | separator: grunt.util.linefeed, 14 | }, 15 | target: { 16 | src: [ 17 | 'src/noise.js', 18 | 'src/core.js', 19 | 'src/images.js', 20 | 'src/filters.js', 21 | 'src/generators.js', 22 | 'src/materials.js', 23 | 'src/scatter.js', 24 | 'src/influences.js', 25 | ], 26 | dest: 'build/THREE.Terrain.js', 27 | nonull: true, 28 | }, 29 | }, 30 | uglify: { 31 | options: { 32 | banner: banner, 33 | compress: { 34 | dead_code: false, 35 | side_effects: false, 36 | unused: false, 37 | }, 38 | mangle: true, 39 | preserveComments: function(node, comment) { 40 | return (/^!/).test(comment.value); 41 | }, 42 | report: 'min', 43 | sourceMap: true, 44 | }, 45 | target: { 46 | files: { 47 | 'build/THREE.Terrain.min.js': ['build/THREE.Terrain.js'], 48 | }, 49 | }, 50 | }, 51 | jshint: { 52 | options: { 53 | trailing: true, 54 | }, 55 | target: { 56 | src: [ 57 | 'demo/index.js', 58 | 'src/noise.js', 59 | 'src/core.js', 60 | 'src/images.js', 61 | 'src/filters.js', 62 | 'src/gaussian.js', 63 | 'src/weightedBoxBlurGaussian.js', 64 | 'src/generators.js', 65 | 'src/materials.js', 66 | 'src/scatter.js', 67 | 'src/influences.js', 68 | 'src/worley.js', 69 | 'src/brownian.js', 70 | 'src/analysis.js', 71 | 'Gruntfile.js', 72 | ], 73 | }, 74 | }, 75 | jscs: { 76 | options: { 77 | config: '.jscs.json', 78 | }, 79 | main: [ 80 | 'demo/index.js', 81 | 'src/noise.js', 82 | 'src/core.js', 83 | 'src/images.js', 84 | 'src/filters.js', 85 | 'src/gaussian.js', 86 | 'src/weightedBoxBlurGaussian.js', 87 | 'src/generators.js', 88 | 'src/materials.js', 89 | 'src/scatter.js', 90 | 'src/influences.js', 91 | 'src/worley.js', 92 | 'src/brownian.js', 93 | 'src/analysis.js', 94 | 'Gruntfile.js', 95 | ], 96 | }, 97 | watch: { 98 | files: [ 99 | 'src/noise.js', 100 | 'src/core.js', 101 | 'src/images.js', 102 | 'src/filters.js', 103 | 'src/gaussian.js', 104 | 'src/weightedBoxBlurGaussian.js', 105 | 'src/generators.js', 106 | 'src/materials.js', 107 | 'src/scatter.js', 108 | 'src/influences.js', 109 | ], 110 | tasks: ['concat', 'uglify'], 111 | }, 112 | }); 113 | 114 | grunt.loadNpmTasks('grunt-contrib-concat'); 115 | grunt.loadNpmTasks('grunt-contrib-uglify'); 116 | grunt.loadNpmTasks('grunt-contrib-jshint'); 117 | grunt.loadNpmTasks('grunt-jscs'); 118 | grunt.loadNpmTasks('grunt-contrib-watch'); 119 | grunt.registerTask('default', ['concat', 'uglify', 'jshint', 'jscs']); 120 | grunt.registerTask('lint', ['jshint', 'jscs']); 121 | }; 122 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2010-2014 Isaac Sukin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/three.terrain.js.svg)](https://www.npmjs.com/package/three.terrain.js) 2 | 3 | `THREE.Terrain` is a **procedural terrain generation engine** for use with the 4 | [Three.js](https://github.com/mrdoob/three.js) 3D graphics library for the web. 5 | 6 | #### [Try the demo](https://icecreamyou.github.io/THREE.Terrain/)! 7 | 8 | ## Usage 9 | 10 | You can download the script normally, install it with Bower (`bower install 11 | THREE.Terrain`), or install it with npm (`npm install three.terrain.js`). To 12 | include it on a page client-side without a module loader: 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | You then have access to the `THREE.Terrain` object. (Make sure the `three.js` 26 | library is loaded first.) 27 | 28 | The latest releases of this project have been tested with three.js 29 | [r130](https://github.com/mrdoob/three.js/releases/tag/r130). 30 | 31 | For compatibility with r160 and later, which require the use of ES Modules, 32 | check out [this fork](https://github.com/oliver408i/THREE.Terrain). 33 | 34 | ### Procedurally Generate a Terrain 35 | 36 | In your own script, generate a terrain and add it to your scene: 37 | 38 | ```javascript 39 | // Generate a terrain 40 | var xS = 63, yS = 63; 41 | terrainScene = THREE.Terrain({ 42 | easing: THREE.Terrain.Linear, 43 | frequency: 2.5, 44 | heightmap: THREE.Terrain.DiamondSquare, 45 | material: new THREE.MeshBasicMaterial({color: 0x5566aa}), 46 | maxHeight: 100, 47 | minHeight: -100, 48 | steps: 1, 49 | xSegments: xS, 50 | xSize: 1024, 51 | ySegments: yS, 52 | ySize: 1024, 53 | }); 54 | // Assuming you already have your global scene, add the terrain to it 55 | scene.add(terrainScene); 56 | 57 | // Optional: 58 | // Get the geometry of the terrain across which you want to scatter meshes 59 | var geo = terrainScene.children[0].geometry; 60 | // Add randomly distributed foliage 61 | decoScene = THREE.Terrain.ScatterMeshes(geo, { 62 | mesh: new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 12, 6)), 63 | w: xS, 64 | h: yS, 65 | spread: 0.02, 66 | randomness: Math.random, 67 | }); 68 | terrainScene.add(decoScene); 69 | ``` 70 | 71 | All parameters are optional and thoroughly documented in the 72 | [source code](https://github.com/IceCreamYou/THREE.Terrain/blob/gh-pages/build/THREE.Terrain.js). 73 | You can play around with some of the parameters and see what happens in the 74 | [demo](https://icecreamyou.github.io/THREE.Terrain/). 75 | 76 | Methods for generating terrain procedurally that are available by default 77 | include Cosine, Diamond-Square (a better version of Midpoint Displacement), 78 | Fault lines, Feature picking, Particle deposition, Perlin and Simplex noise, 79 | Value noise, Weierstrass functions, Worley noise (aka Cell or Voronoi noise), 80 | Brownian motion, arbitrary curves, and various combinations of those. 81 | 82 | ### Exporting and Importing 83 | 84 | Export a terrain to a heightmap image: 85 | 86 | ```javascript 87 | // Returns a canvas with the heightmap drawn on it. 88 | // Append to your document body to view; right click to save as a PNG image. 89 | var canvas = THREE.Terrain.toHeightmap( 90 | // terrainScene.children[0] is the most detailed version of the terrain mesh 91 | terrainScene.children[0].geometry.attributes.position.array, 92 | { xSegments: 63, ySegments: 63 } 93 | ); 94 | ``` 95 | 96 | The result will look something like this: 97 | 98 | ![Heightmap](https://raw.githubusercontent.com/IceCreamYou/THREE.Terrain/gh-pages/demo/img/heightmap.png) 99 | 100 | If all you need is a static terrain, the easiest way to generate a heightmap is 101 | to use the [demo](https://icecreamyou.github.io/THREE.Terrain/) and save the 102 | generated heightmap that appears in the upper-left corner. However, if you want 103 | to perform custom manipulations on the terrain first, you will need to export 104 | the heightmap yourself. 105 | 106 | To import a heightmap, create a terrain as explained above, but pass the loaded 107 | heightmap image (or a canvas containing a heightmap) to the `heightmap` option 108 | for the `THREE.Terrain()` function (instead of passing a procedural generation 109 | function). 110 | 111 | ### Dynamic Terrain Materials 112 | 113 | When generating terrains procedurally, it's useful to automatically texture 114 | terrains based on elevation/biome, slope, and location. A utility function is 115 | provided that generates such a material (other than blending textures together, 116 | it is the same as a `MeshLambertMaterial`). 117 | 118 | ```javascript 119 | // t1, t2, t3, and t4 must be textures, e.g. loaded using `THREE.TextureLoader.load()`. 120 | // The function takes an array specifying textures to blend together and how to do so. 121 | // The `levels` property indicates at what height to blend the texture in and out. 122 | // The `glsl` property allows specifying a GLSL expression for texture blending. 123 | var material = THREE.Terrain.generateBlendedMaterial([ 124 | // The first texture is the base; other textures are blended in on top. 125 | { texture: t1 }, 126 | // Start blending in at height -80; opaque between -35 and 20; blend out by 50 127 | { texture: t2, levels: [-80, -35, 20, 50] }, 128 | { texture: t3, levels: [20, 50, 60, 85] }, 129 | // How quickly this texture is blended in depends on its x-position. 130 | { texture: t4, glsl: '1.0 - smoothstep(65.0 + smoothstep(-256.0, 256.0, vPosition.x) * 10.0, 80.0, vPosition.z)' }, 131 | // Use this texture if the slope is between 27 and 45 degrees 132 | { texture: t3, glsl: 'slope > 0.7853981633974483 ? 0.2 : 1.0 - smoothstep(0.47123889803846897, 0.7853981633974483, slope) + 0.2' }, 133 | ]); 134 | ``` 135 | 136 | ### More 137 | 138 | Many other utilities are provided, for example for compositing different 139 | terrain generation methods; creating islands, cliffs, canyons, and plateaus; 140 | manually influencing the terrain's shape at different locations; different 141 | kinds of smoothing; and more. These features are all fully documented in the 142 | [source code](https://github.com/IceCreamYou/THREE.Terrain/blob/gh-pages/build/THREE.Terrain.js). 143 | Additionally, you can create custom methods for generating terrain or affecting 144 | other processes. 145 | 146 | There is also a 147 | [simulation](https://github.com/IceCreamYou/THREE.Terrain/tree/gh-pages/statistics) 148 | included that calculates statistics about each major procedural terrain 149 | generation method included in the `THREE.Terrain` library. 150 | 151 | ## Screenshots 152 | 153 | ![Screenshot 1](https://raw.githubusercontent.com/IceCreamYou/THREE.Terrain/gh-pages/demo/img/screenshot1.jpg) 154 | ![Screenshot 2](https://raw.githubusercontent.com/IceCreamYou/THREE.Terrain/gh-pages/demo/img/screenshot2.jpg) 155 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "THREE.Terrain", 3 | "version": "1.6.0", 4 | "main": "build/THREE.Terrain.min.js", 5 | "ignore": [ 6 | "node_modules" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /build/THREE.Terrain.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THREE.Terrain.js 2.0.0-20220705 3 | * 4 | * @author Isaac Sukin (http://www.isaacsukin.com/) 5 | * @license MIT 6 | */ 7 | !function(e){var e=e.noise={};function r(e,r,n){this.x=e,this.y=r,this.z=n}r.prototype.dot2=function(e,r){return this.x*e+this.y*r},r.prototype.dot3=function(e,r,n){return this.x*e+this.y*r+this.z*n};var t=[new r(1,1,0),new r(-1,1,0),new r(1,-1,0),new r(-1,-1,0),new r(1,0,1),new r(-1,0,1),new r(1,0,-1),new r(-1,0,-1),new r(0,1,1),new r(0,-1,1),new r(0,1,-1),new r(0,-1,-1)],a=[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180],E=new Array(512),u=new Array(512),g=(e.seed=function(e){0>8&255;E[r]=E[r+256]=n,u[r]=u[r+256]=t[n%12]}},e.seed(Math.random()),.5*(Math.sqrt(3)-1)),T=(3-Math.sqrt(3))/6,n=1/3,i=1/6;function m(e){return e*e*e*(e*(6*e-15)+10)}function h(e,r,n){return(1-n)*e+n*r}e.simplex=function(e,r){var n,t,r,n=(e+r)*g,a=Math.floor(e+n),n=Math.floor(r+n),i=(a+n)*T,e=e-a+i,r=r-n+i,o,i,i=ro&&(o=e[h]),e[h]t&&(t=e[i]);var o=t-n,m="number"!=typeof r.maxHeight?t:r.maxHeight,h="number"!=typeof r.minHeight?n:r.minHeight,s=!r.stretch&&th&&(h=e[u]))}var g=m*i+a,T,l,h,s;"number"==typeof n&&(h=(l=s+(T=.5*(h-s)))+T*n,s=l-T*n),t[g]=e[g]>h?h:e[g]=m[t].min&&E<=m[t].max){e[n]=m[t].avg;break}}},THREE.Terrain.Turbulence=function(e,r){for(var n=r.maxHeight-r.minHeight,t=0,a=e.length;t","varying vec2 MyvUv;\nvarying vec3 vPosition;\nvarying vec3 myNormal;\n#include "),e.vertexShader=e.vertexShader.replace("#include ","MyvUv = uv;\nvPosition = position;\nmyNormal = normal;\n#include "),e.fragmentShader=e.fragmentShader.replace("#include ",c+"\n#include "),e.fragmentShader=e.fragmentShader.replace("#include ",l);for(var r=0,n=t.length;rMath.random()):c=r.spread(s,l/9,u,l),c&&(u.angleTo(g)>r.maxSlope||((d=r.mesh.clone()).position.addVectors(s,f).add(E).divideScalar(3),0r.maxTilt&&(c=r.maxTilt/c,d.rotation.x*=c,d.rotation.y*=c,d.rotation.z*=c)),d.rotation.x+=.5*Math.PI,d.rotateY(2*Math.random()*Math.PI),r.sizeVariance&&(c=Math.random()*h-r.sizeVariance,d.scale.x=d.scale.z=1+c,d.scale.y+=c),d.updateMatrix(),r.scene.add(d)))}return r.scene},THREE.Terrain.ScatterHelper=function(e,r,n,t){n=n||1,t=t||.25,r.frequency=r.frequency||2.5;var a={},i;for(i in r)r.hasOwnProperty(i)&&(a[i]=r[i]);a.xSegments*=2,a.stretch=!0,a.maxHeight=1,a.minHeight=0;for(var o=THREE.Terrain.heightmapArray(e,a),m=0,h=o.length;mt)&&(o[m]=1);return function(){return o}},THREE.Terrain.Influences={Mesa:function(e){return 1.25*Math.min(.8,Math.exp(-e*e))},Hole:function(e){return-THREE.Terrain.Influences.Mesa(e)},Hill:function(e){return e<0?(e+1)*(e+1)*(3-2*(e+1)):1-e*e*(3-2*e)},Valley:function(e){return-THREE.Terrain.Influences.Hill(e)},Dome:function(e){return-(e+1)*(e-1)},Flat:function(e){return 0},Volcano:function(e){return.94-.32*(Math.abs(2*e)+Math.cos(2*Math.PI*Math.abs(e)+.4))}},THREE.Terrain.Influence=function(e,r,n,t,a,i,o,m,h){n=n||THREE.Terrain.Influences.Hill,t=void 0===t?.5:t,a=void 0===a?.5:a,i=void 0===i?64:i,o=void 0===o?64:o,m=void 0===m?THREE.NormalBlending:m,h=h||THREE.Terrain.EaseIn;for(var s=r.xSegments+1,f,E=s*t,u=(r.ySegments+1)*a,g=r.xSize/r.xSegments,T=r.ySize/r.ySegments,t=i/g,a=i/T,l=1/i,r=Math.ceil(E-t),c=Math.floor(E+t),d=Math.ceil(u-a),H=Math.floor(u+a),v=r;v=g+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/ 4 | 1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v); 5 | b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f}); 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Terrain 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Analyze
18 |
19 |

Analytics

20 | ? 21 |
22 |
23 |

Summary

24 | 25 | 26 | 27 | 28 | 29 | 30 |
Elevation dispersion
Elevation skew
Jaggedness
31 |
32 |
33 |

 

34 | 35 | 36 | 37 | 38 | 39 | 40 |
Slope dispersion
Slope skew
Ruggedness
41 |
42 |
43 |
44 |
45 |

Elevation

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Sample size
Min
Max
Range
Midrange
Median
IQR
Mean
Std dev
MAD
Mode
Pearson skew
G&M skew
Kurtosis
65 |
66 | 67 |
Elevation Histogram
68 |
69 |
70 |
71 |

Slope

72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
Sample size
Min
Max
Range
Midrange
Median
IQR
Mean
Std dev
MAD
Mode
Pearson skew
G&M skew
Kurtosis
90 |
91 | 92 |
Slope Histogram
93 |
94 |
95 |
96 |
97 |
98 |

Roughness metrics

99 | 100 | 101 | 102 | 103 | 104 | 105 |
2D to 3D area ratio
Ruggedness Index
Jaggedness
106 |
107 |
108 |

Fitted Plane

109 | 110 | 111 | 112 | 113 | 114 |
Slope
Var. explained
115 |
116 |
117 |
118 | Close 119 |
120 |
121 | 122 |
W = Forward, S = Back, A = Left, D = Right, R = Up, F = Down, Q = Freeze, Mouse = Look
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three.terrain.js", 3 | "version": "2.0.0", 4 | "description": "Extends the Three.js web-based 3D graphics framework to support generating random terrains and rendering terrain from predetermined heightmaps.", 5 | "homepage": "https://github.com/IceCreamYou/THREE.Terrain", 6 | "bugs": "https://github.com/IceCreamYou/THREE.Terrain/issues", 7 | "main": "build/THREE.Terrain.min.js", 8 | "directories": { 9 | "lib": "build", 10 | "example": "demo" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/IceCreamYou/THREE.Terrain.git" 15 | }, 16 | "keywords": [ 17 | "THREE", 18 | "three.js", 19 | "terrain", 20 | "land", 21 | "DiamondSquare", 22 | "random", 23 | "generator" 24 | ], 25 | "author": "Isaac Sukin (http://www.isaacsukin.com/)", 26 | "license": "MIT", 27 | "readmeFilename": "README.md", 28 | "devDependencies": { 29 | "grunt": "^1.4.1", 30 | "grunt-contrib-concat": "^1.0.1", 31 | "grunt-contrib-jshint": "^2.1.0", 32 | "grunt-contrib-uglify": "^5.0.1", 33 | "grunt-contrib-watch": "^1.1.0", 34 | "grunt-jscs": "^3.0.1", 35 | "three": "^0.130.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | This roadmap is aspirational - a list of ideas more so than a description of intent. 2 | 3 | ## 3.0 4 | 5 | - Make the API more consistent: naming (e.g. use "elevation" instead of "height"), capitalization, method and property grouping 6 | - Write documentation that's not in the code 7 | - Minify JS in /src that isn't included in the main bundle 8 | - Fix Smoothing to use a smoothing factor (a multiplier for how close the point should move to the average) instead of a weight for the target point 9 | - Add the ability for Smoothing to look in a broader neighborhood, not just the immediately surrounding 8 points 10 | - Implement helper functions to convert from a 1D Vector3 array to/from a 1D and 2D float array, and convert the generator and filter functions to operate on those 11 | - Phong lighting for generated textures 12 | - Allow making slopes rougher than flats 13 | - Create modified smoothing functions that apply at different intensities depending on the slope 14 | - Create a filter that supports compositing another heightmap / the result of a procedural function over the terrain at different intensities depending on the existing slope at each vertex (a generalization of the above) 15 | - Or create a procedural function that randomly adjusts the height of vertices with different amplitude based on their slopes 16 | - Add a method to get the terrain height at a given spatial location 17 | - The best way to do this is probably with a raycaster 18 | - Make scattering be based on spatial distance, not faces 19 | - This probably looks something like Voronoi cells 20 | - Add a function to horizontally shift the high points of high slope faces to possibly generate some overhang 21 | 22 | 23 | ## 3.1 24 | 25 | - Make FirstPersonControls rotate on swipe and move forward on tap-and-hold like OrbitControls 26 | - Try using the terrain with a physics library 27 | - Support morphing over time between two heightmaps 28 | - Support manually sculpting (raising/lowering) terrain 29 | - Look into writing a space partitioning algorithm (like the way procedural dungeons are often built) and shape a terrain around that 30 | - Investigate search-based and agent-based terrain generation http://pcgbook.com/wp-content/uploads/chapter04.pdf 31 | - Provide some sort of grammar for to guide terrain generation based on objectives? 32 | - Provide some mechanism for evolution towards finding a terrain that most closely meets a set of rules? 33 | - Try IFFT(LowPassFilter(FFT(WhiteNoise()))) again as a procedural generation method 34 | - Try simulating techtonic plates as a procedural generation method as described at https://webcache.googleusercontent.com/search?q=cache:http://experilous.com/1/blog/post/procedural-planet-generation 35 | - Support a terrain "mask" for creating holes 36 | 37 | 38 | ## 3.2 39 | 40 | - Allow terrain generators and filters to partially apply by returning promises, to enable watching while the terrain is transformed 41 | 42 | 43 | ## 4.0 44 | 45 | ``` 46 | Erosion 47 | Clone the terrain 48 | For each original face with a slope above the angle of repose 49 | Reduce changes in elevation (by raising lower vertices and lowering higher vertices) to reach the angle of repose 50 | Set those new elevations in the clone 51 | Set the original to the clone 52 | Repeat until no changes are made 53 | Flooding 54 | Methods: 55 | Sea level rise 56 | Set a maximum flood height 57 | For each point in the heightmap that hasn't been included in a flood-fill that doesn't touch an edge yet 58 | Discard the point if it is above the max flood height 59 | Flood-fill up to the point's height 60 | Mark if the flood touches an edge 61 | If a lower flood is encountered when flood-filling 62 | If the lower flood doesn't touch an edge, add it to the current flood and keep track of it 63 | Otherwise add it to the current flood, discard it, and mark the current flood as touching an edge 64 | If the higher flood ends up not touching an edge, delete the lower flood 65 | Otherwise delete the higher flood but not the lower flood 66 | Walk over the flood-fills 67 | Any flood-fill that doesn't touch an edge is a pond 68 | Rain 69 | Simulate units of water falling uniformly over the plane 70 | Each drop of water flows toward the local minimum down the path of steepest descent and accumulates 71 | Need to account for ponds overflowing 72 | Minima 73 | Find all the local minima, where a minimum is the lowest point in a flood-fill starting from that point with a given minimum area and the flood-fill doesn't touch the edge 74 | To find, sort vertices by height, then test each one (discarding flooded vertices during the flood-fill phase) 75 | For each minimum, find the lowest height that will spill to an edge by flood-filling at successive heights 76 | For each minimum, flood up to a given percentage of the lowest spill point 77 | When ponds are discovered, water planes need to be created for them 78 | River generation 79 | Methods: 80 | Pick random (high-elevation) origin locations and let particles flow downward 81 | Use Brownian trees https://en.wikipedia.org/wiki/Brownian_tree 82 | A nice shape for displacement around river paths is -e^(-(2x)^2): http://www.wolframalpha.com/input/?i=-e^%28-%282x%29^2%29+from+-1+to+1 83 | Water planes need to be created to match river shapes 84 | Account for ending in a pond instead of at an edge 85 | Make rivers narrower and shallower at the top 86 | ``` 87 | 88 | 89 | ## 5.0 90 | 91 | - Implement optimization types 92 | - Support infinite terrain 93 | - Try implementing spherical (planet) generation with biomes 94 | - Tunnels and caves 95 | 96 | 97 | ## Known bugs 98 | 99 | - THREE.Terrain.Gaussian() fails to smooth one edge of the terrain, resulting in some weird artifacts. 100 | -------------------------------------------------------------------------------- /src/analysis.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | /** 4 | * Analyze a terrain using statistical measures. 5 | * 6 | * @param {THREE.Mesh} mesh 7 | * The terrain mesh to analyze. 8 | * @param {Object} options 9 | * The map of settings that were passed to `THREE.Terrain()` to construct the 10 | * terrain mesh that is being analyzed. Requires at least `maxHeight`, 11 | * `minHeight`, `xSegments`, `xSize`, `ySegments`, and `ySize` properties. 12 | * 13 | * @return {Object} 14 | * An object containing statistical information about the terrain. 15 | */ 16 | THREE.Terrain.Analyze = function(mesh, options) { 17 | if (mesh.geometry.attributes.position.count < 3) { 18 | throw new Error('Not enough vertices to analyze'); 19 | } 20 | 21 | var sortNumeric = function(a, b) { return a - b; }, 22 | elevations = Array.prototype.sort.call( 23 | THREE.Terrain.toArray1D(mesh.geometry.attributes.position.array), 24 | sortNumeric 25 | ), 26 | numVertices = elevations.length, 27 | maxElevation = percentile(elevations, 1), 28 | minElevation = percentile(elevations, 0), 29 | medianElevation = percentile(elevations, 0.5), 30 | meanElevation = mean(elevations), 31 | stdevElevation = 0, 32 | pearsonSkewElevation = 0, 33 | groeneveldMeedenSkewElevation = 0, 34 | kurtosisElevation = 0, 35 | up = mesh.up.clone().applyAxisAngle(new THREE.Vector3(1, 0, 0), 0.5*Math.PI), // correct for mesh rotation 36 | slopes = faceNormals(mesh.geometry, options) 37 | .map(function(normal) { return normal.angleTo(up) * 180 / Math.PI; }) 38 | .sort(sortNumeric), 39 | numFaces = slopes.length, 40 | maxSlope = percentile(slopes, 1), 41 | minSlope = percentile(slopes, 0), 42 | medianSlope = percentile(slopes, 0.5), 43 | meanSlope = mean(slopes), 44 | centroid = mesh.position.clone().setZ(meanElevation), 45 | fittedPlaneNormal = getFittedPlaneNormal(mesh.geometry.attributes.position.array, centroid), 46 | fittedPlaneSlope = fittedPlaneNormal.angleTo(up) * 180 / Math.PI, 47 | stdevSlope = 0, 48 | pearsonSkewSlope = 0, 49 | groeneveldMeedenSkewSlope = 0, 50 | kurtosisSlope = 0, 51 | faceArea2D = (options.xSize / options.xSegments) * (options.ySize / options.ySegments) * 0.5, 52 | area3D = 0, 53 | tri = 0, 54 | jaggedness = 0, 55 | medianElevationDeviations = new Float32Array(numVertices), 56 | medianSlopeDeviations = new Float32Array(numFaces), 57 | deviation, 58 | i; 59 | 60 | for (i = 0; i < numVertices; i++) { 61 | deviation = elevations[i] - meanElevation; 62 | stdevElevation += deviation * deviation; 63 | pearsonSkewElevation += deviation * deviation * deviation; 64 | medianElevationDeviations[i] = Math.abs(elevations[i] - medianElevation); 65 | groeneveldMeedenSkewElevation += medianElevationDeviations[i]; 66 | kurtosisElevation += deviation * deviation * deviation * deviation; 67 | } 68 | pearsonSkewElevation = (pearsonSkewElevation / numVertices) / Math.pow(stdevElevation / (numVertices - 1), 1.5); 69 | groeneveldMeedenSkewElevation = (meanElevation - medianElevation) / (groeneveldMeedenSkewElevation / numVertices); 70 | kurtosisElevation = (kurtosisElevation * numVertices) / (stdevElevation * stdevElevation) - 3; 71 | stdevElevation = Math.sqrt(stdevElevation / numVertices); 72 | Array.prototype.sort.call(medianElevationDeviations, sortNumeric); 73 | 74 | for (i = 0; i < numFaces; i++) { 75 | deviation = slopes[i] - meanSlope; 76 | stdevSlope += deviation * deviation; 77 | pearsonSkewSlope += deviation * deviation * deviation; 78 | medianSlopeDeviations[i] = Math.abs(slopes[i] - medianSlope); 79 | groeneveldMeedenSkewSlope += medianSlopeDeviations[i]; 80 | kurtosisSlope += deviation * deviation * deviation * deviation; 81 | area3D += faceArea2D / Math.cos(slopes[i] * Math.PI / 180); 82 | } 83 | pearsonSkewSlope = (pearsonSkewSlope / numFaces) / Math.pow(stdevSlope / (numFaces - 1), 1.5); 84 | groeneveldMeedenSkewSlope = (meanSlope - medianSlope) / (groeneveldMeedenSkewSlope / numFaces); 85 | kurtosisSlope = (kurtosisSlope * numFaces) / (stdevSlope * stdevSlope) - 3; 86 | stdevSlope = Math.sqrt(stdevSlope / numFaces); 87 | Array.prototype.sort.call(medianSlopeDeviations, sortNumeric); 88 | 89 | for (var ii = 0, xl = options.xSegments + 1, yl = options.ySegments + 1; ii < xl; ii++) { 90 | for (var j = 0; j < yl; j++) { 91 | var neighborhoodMax = -Infinity, 92 | neighborhoodMin = Infinity, 93 | v = mesh.geometry.attributes.position.array[(j*xl + ii) * 3 + 2], 94 | sum = 0, 95 | c = 0; 96 | for (var n = -1; n <= 1; n++) { 97 | for (var m = -1; m <= 1; m++) { 98 | if (ii+m >= 0 && j+n >= 0 && ii+m < xl && j+n < yl && !(n === 0 && m === 0)) { 99 | var val = mesh.geometry.attributes.position.array[((j+n)*xl + ii + m) * 3 + 2]; 100 | sum += val; 101 | c++; 102 | if (val > neighborhoodMax) neighborhoodMax = val; 103 | if (val < neighborhoodMin) neighborhoodMin = val; 104 | } 105 | } 106 | } 107 | if (c) tri += (sum / c - v) * (sum / c - v); 108 | if (v > neighborhoodMax || v < neighborhoodMin) jaggedness++; 109 | } 110 | } 111 | tri = Math.sqrt(tri / numVertices); 112 | // ceil(n/2)*ceil(m/2) is the max # of local maxima or minima in an n*m grid 113 | jaggedness /= Math.ceil((options.xSegments+1) * 0.5) * Math.ceil((options.ySegments+1) * 0.5) * 2; 114 | 115 | return { 116 | elevation: { 117 | sampleSize: numVertices, 118 | max: maxElevation, 119 | min: minElevation, 120 | range: maxElevation - minElevation, 121 | midrange: (maxElevation - minElevation) * 0.5 + minElevation, 122 | median: medianElevation, 123 | iqr: percentile(elevations, 0.75) - percentile(elevations, 0.25), 124 | mean: meanElevation, 125 | stdev: stdevElevation, 126 | mad: percentile(medianElevationDeviations, 0.5), 127 | pearsonSkew: pearsonSkewElevation, 128 | groeneveldMeedenSkew: groeneveldMeedenSkewElevation, 129 | kurtosis: kurtosisElevation, 130 | modes: getModes( 131 | elevations, 132 | Math.ceil(options.maxHeight - options.minHeight), 133 | options.minHeight, 134 | options.maxHeight 135 | ), 136 | percentile: function(p) { return percentile(elevations, p); }, 137 | percentRank: function(v) { return percentRank(elevations, v); }, 138 | drawHistogram: function(canvas, bucketCount) { 139 | drawHistogram( 140 | bucketNumbersLinearly( 141 | elevations, 142 | bucketCount, 143 | options.minHeight, 144 | options.maxHeight 145 | ), 146 | canvas, 147 | options.minHeight, 148 | options.maxHeight 149 | ); 150 | }, 151 | }, 152 | slope: { 153 | sampleSize: numFaces, 154 | max: maxSlope, 155 | min: minSlope, 156 | range: maxSlope - minSlope, 157 | midrange: (maxSlope - minSlope) * 0.5 + minSlope, 158 | median: medianSlope, 159 | iqr: percentile(slopes, 0.75) - percentile(slopes, 0.25), 160 | mean: meanSlope, 161 | stdev: stdevSlope, 162 | mad: percentile(medianSlopeDeviations, 0.5), 163 | pearsonSkew: pearsonSkewSlope, 164 | groeneveldMeedenSkew: groeneveldMeedenSkewSlope, 165 | kurtosis: kurtosisSlope, 166 | modes: getModes(slopes, 90, 0, 90), 167 | percentile: function(p) { return percentile(slopes, p); }, 168 | percentRank: function(v) { return percentRank(slopes, v); }, 169 | drawHistogram: function(canvas, bucketCount) { 170 | drawHistogram( 171 | bucketNumbersLinearly( 172 | slopes, 173 | bucketCount, 174 | 0, 175 | 90 176 | ), 177 | canvas, 178 | 0, 179 | 90, 180 | String.fromCharCode(176) 181 | ); 182 | }, 183 | }, 184 | roughness: { 185 | planimetricAreaRatio: options.xSize * options.ySize / area3D, 186 | terrainRuggednessIndex: tri, 187 | jaggedness: jaggedness, 188 | }, 189 | fittedPlane: { 190 | centroid: centroid, 191 | normal: fittedPlaneNormal, 192 | slope: fittedPlaneSlope, 193 | pctExplained: percentVariationExplainedByFittedPlane( 194 | mesh.geometry.attributes.position.array, 195 | centroid, 196 | fittedPlaneNormal, 197 | options.maxHeight - options.minHeight 198 | ), 199 | }, 200 | // # of different kinds of features http://www.armystudyguide.com/content/army_board_study_guide_topics/land_navigation_map_reading/identify-major-minor-terr.shtml 201 | }; 202 | }; 203 | 204 | /** 205 | * Returns the value at a given percentile in a sorted numeric array. 206 | * 207 | * Uses the "linear interpolation between closest ranks" method. 208 | * 209 | * @param {Number[]} arr 210 | * A sorted numeric array to examine. 211 | * @param {Number} p 212 | * The percentile at which to return the value. 213 | * 214 | * @return {Number} 215 | * The value at the given percentile in the given array. 216 | */ 217 | function percentile(arr, p) { 218 | if (arr.length === 0) return 0; 219 | if (typeof p !== 'number') throw new TypeError('p must be a number'); 220 | if (p <= 0) return arr[0]; 221 | if (p >= 1) return arr[arr.length - 1]; 222 | 223 | var index = arr.length * p, 224 | lower = Math.floor(index), 225 | upper = lower + 1, 226 | weight = index % 1; 227 | 228 | if (upper >= arr.length) return arr[lower]; 229 | return arr[lower] * (1 - weight) + arr[upper] * weight; 230 | } 231 | 232 | /** 233 | * Returns the percentile of the given value in a sorted numeric array. 234 | * 235 | * @param {Number[]} arr 236 | * A sorted numeric array to examine. 237 | * @param {Number} v 238 | * The value at which to return the percentile. 239 | * 240 | * @return {Number} 241 | * The percentile at the given value in the given array. 242 | */ 243 | function percentRank(arr, v) { 244 | if (typeof v !== 'number') throw new TypeError('v must be a number'); 245 | for (var i = 0, l = arr.length; i < l; i++) { 246 | if (v <= arr[i]) { 247 | while (i < l && v === arr[i]) { 248 | i++; 249 | } 250 | if (i === 0) return 0; 251 | if (v !== arr[i-1]) { 252 | i += (v - arr[i-1]) / (arr[i] - arr[i-1]); 253 | } 254 | return i / l; 255 | } 256 | } 257 | return 1; 258 | } 259 | 260 | /** 261 | * Returns the face normals for the specified geometry. 262 | * 263 | * @param {THREE.BufferGeometry} geometry 264 | * The indexed geometry to analyze. 265 | * @param {Object} options 266 | * Includes the `xSegments` and `ySegments` - the number of row and column 267 | * segments of the plane geometry. 268 | */ 269 | function faceNormals(geometry, options) { 270 | geometry = geometry.toNonIndexed(); 271 | var normals = new Array(Math.round(geometry.attributes.position.array.length / 9)), 272 | gArray = geometry.attributes.position.array, 273 | vertex1 = new THREE.Vector3(), 274 | vertex2 = new THREE.Vector3(), 275 | vertex3 = new THREE.Vector3(); 276 | 277 | for (var i = 0, j = 0; i < geometry.attributes.position.array.length; i += 9, j++) { 278 | vertex1.set(gArray[i + 0], gArray[i + 1], gArray[i + 2]); 279 | vertex2.set(gArray[i + 3], gArray[i + 4], gArray[i + 5]); 280 | vertex3.set(gArray[i + 6], gArray[i + 7], gArray[i + 8]); 281 | 282 | var faceNormal = new THREE.Vector3(); 283 | THREE.Triangle.getNormal(vertex1, vertex2, vertex3, faceNormal); 284 | normals[j] = faceNormal; 285 | } 286 | return normals; 287 | } 288 | 289 | /** 290 | * Gets the normal vector of the fitted plane of a 3D array of points. 291 | * 292 | * @param {Float32Array} points 293 | * The vertex positions of the geometry to analyze. 294 | * @param {THREE.Vector3} centroid 295 | * The centroid of the vertex cloud. 296 | * 297 | * @return {THREE.Vector3} 298 | * The normal vector of the fitted plane. 299 | */ 300 | function getFittedPlaneNormal(points, centroid) { 301 | var n = points.length, 302 | xx = 0, 303 | xy = 0, 304 | xz = 0, 305 | yy = 0, 306 | yz = 0, 307 | zz = 0; 308 | if (n < 3) throw new Error('At least three points are required to fit a plane'); 309 | 310 | var r = new THREE.Vector3(); 311 | for (var i = 0, l = points.length; i < l; i += 3) { 312 | r.set(points[i], points[i+1], points[i+2]).sub(centroid); 313 | xx += r.x * r.x; 314 | xy += r.x * r.y; 315 | xz += r.x * r.z; 316 | yy += r.y * r.y; 317 | yz += r.y * r.z; 318 | zz += r.z * r.z; 319 | } 320 | 321 | var xDeterminant = yy*zz - yz*yz, 322 | yDeterminant = xx*zz - xz*xz, 323 | zDeterminant = xx*yy - xy*xy, 324 | maxDeterminant = Math.max(xDeterminant, yDeterminant, zDeterminant); 325 | if (maxDeterminant <= 0) throw new Error("The points don't span a plane"); 326 | 327 | if (maxDeterminant === xDeterminant) { 328 | r.set( 329 | 1, 330 | (xz*yz - xy*zz) / xDeterminant, 331 | (xy*yz - xz*yy) / xDeterminant 332 | ); 333 | } 334 | else if (maxDeterminant === yDeterminant) { 335 | r.set( 336 | (yz*xz - xy*zz) / yDeterminant, 337 | 1, 338 | (xy*xz - yz*xx) / yDeterminant 339 | ); 340 | } 341 | else if (maxDeterminant === zDeterminant) { 342 | r.set( 343 | (yz*xy - xz*yy) / zDeterminant, 344 | (xz*xy - yz*xx) / zDeterminant, 345 | 1 346 | ); 347 | } 348 | return r.normalize(); 349 | } 350 | 351 | /** 352 | * Put numbers into buckets that have equal-size ranges. 353 | * 354 | * @param {Number[]} data 355 | * The data to bucket. 356 | * @param {Number} bucketCount 357 | * The number of buckets to use. 358 | * @param {Number} [min] 359 | * The minimum allowed data value. Defaults to the smallest value passed. 360 | * @param {Number} [max] 361 | * The maximum allowed data value. Defaults to the largest value passed. 362 | * 363 | * @return {Number[][]} An array of buckets of numbers. 364 | */ 365 | function bucketNumbersLinearly(data, bucketCount, min, max) { 366 | var i = 0, 367 | l = data.length; 368 | // If min and max aren't given, set them to the highest and lowest data values 369 | if (typeof min === 'undefined') { 370 | min = Infinity; 371 | max = -Infinity; 372 | for (i = 0; i < l; i++) { 373 | if (data[i] < min) min = data[i]; 374 | if (data[i] > max) max = data[i]; 375 | } 376 | } 377 | var inc = (max - min) / bucketCount, 378 | buckets = new Array(bucketCount); 379 | // Initialize buckets 380 | for (i = 0; i < bucketCount; i++) { 381 | buckets[i] = []; 382 | } 383 | // Put the numbers into buckets 384 | for (i = 0; i < l; i++) { 385 | // Buckets include the lower bound but not the higher bound, except the top bucket 386 | try { 387 | if (data[i] === max) buckets[bucketCount-1].push(data[i]); 388 | else buckets[((data[i] - min) / inc) | 0].push(data[i]); 389 | } catch(e) { 390 | console.warn('Numbers in the data are outside of the min and max values used to bucket the data.'); 391 | } 392 | } 393 | return buckets; 394 | } 395 | 396 | /** 397 | * Get the bucketed mode(s) in a data set. 398 | * 399 | * @param {Number[]} data 400 | * The data set from which the modes should be retrieved. 401 | * @param {Number} bucketCount 402 | * The number of buckets to use. 403 | * @param {Number} min 404 | * The minimum allowed data value. 405 | * @param {Number} max 406 | * The maximum allowed data value. 407 | * 408 | * @return {Number[]} 409 | * An array containing the bucketed mode(s). 410 | */ 411 | function getModes(data, bucketCount, min, max) { 412 | var buckets = bucketNumbersLinearly(data, bucketCount, min, max), 413 | maxLen = 0, 414 | modes = []; 415 | for (var i = 0, l = buckets.length; i < l; i++) { 416 | if (buckets[i].length > maxLen) { 417 | maxLen = buckets[i].length; 418 | modes = [Math.floor(((i + 0.5) / l) * (max - min) + min)]; 419 | } 420 | else if (buckets[i].length === maxLen) { 421 | modes.push(Math.floor(((i + 0.5) / l) * (max - min) + min)); 422 | } 423 | } 424 | return modes; 425 | } 426 | 427 | /** 428 | * Draw a histogram. 429 | * 430 | * @param {Number[][]} buckets 431 | * An array of data to draw, typically from `bucketNumbersLinearly()`. 432 | * @param {HTMLCanvasElement} canvas 433 | * The canvas on which to draw the histogram. 434 | * @param {Number} [minV] 435 | * The lowest x-value to plot. Defaults to the lowest value in the data. 436 | * @param {Number} [maxV] 437 | * The highest x-value to plot. Defaults to the highest value in the data. 438 | * @param {String} [append=''] 439 | * A string to append to the bar labels. Defaults to the empty string. 440 | */ 441 | function drawHistogram(buckets, canvas, minV, maxV, append) { 442 | var context = canvas.getContext('2d'), 443 | width = 280, 444 | height = 180, 445 | border = 10, 446 | separator = 4, 447 | max = typeof maxV === 'undefined' ? -Infinity : maxV, 448 | min = typeof minV === 'undefined' ? Infinity : minV, 449 | l = buckets.length, 450 | i; 451 | canvas.width = width + border*2; 452 | canvas.height = height + border*2; 453 | if (typeof append === 'undefined') append = ''; 454 | 455 | // If max or min is not set, set them to the highest/lowest value. 456 | if (max === -Infinity || min === Infinity) { 457 | for (i = 0; i < l; i++) { 458 | for (var j = 0, m = buckets[i].length; j < m; j++) { 459 | if (buckets[i][j] > max) { 460 | max = buckets[i][j]; 461 | } 462 | if (buckets[i][j] < min) { 463 | min = buckets[i][j]; 464 | } 465 | } 466 | } 467 | } 468 | 469 | // Find the size of the largest bucket. 470 | var maxBucketSize = 0, 471 | n = 0; 472 | for (i = 0; i < l; i++) { 473 | if (buckets[i].length > maxBucketSize) { 474 | maxBucketSize = buckets[i].length; 475 | } 476 | n += buckets[i].length; 477 | } 478 | 479 | // Draw a bar. 480 | var unitSizeY = (height - separator) / maxBucketSize, 481 | unitSizeX = (width - (buckets.length + 1) * separator) / buckets.length; 482 | if (unitSizeX >= 1) unitSizeX = Math.floor(unitSizeX); 483 | if (unitSizeY >= 1) unitSizeY = Math.floor(unitSizeY); 484 | context.fillStyle = 'rgba(13, 42, 64, 1)'; 485 | for (i = 0; i < l; i++) { 486 | context.fillRect( 487 | border + separator + i * (unitSizeX + separator), 488 | border + height - (separator + buckets[i].length * unitSizeY), 489 | unitSizeX, 490 | unitSizeY * buckets[i].length 491 | ); 492 | } 493 | 494 | // Draw the label text on the bar. 495 | context.fillStyle = 'rgba(144, 176, 192, 1)'; 496 | context.font = '12px Arial'; 497 | for (i = 0; i < l; i++) { 498 | var text = Math.floor(((i + 0.5) / buckets.length) * (max - min) + min) + '' + append; 499 | context.fillText( 500 | text, 501 | border + separator + i * (unitSizeX + separator) + Math.floor((unitSizeX - context.measureText(text).width) * 0.5), 502 | border + height - 8, 503 | unitSizeX 504 | ); 505 | } 506 | 507 | context.fillText( 508 | Math.round(100 * maxBucketSize / n) + '%', 509 | border + separator, 510 | border + separator + 6 511 | ); 512 | 513 | // Draw axes. 514 | context.strokeStyle = 'rgba(13, 42, 64, 1)'; 515 | context.lineWidth = 2; 516 | context.beginPath(); 517 | context.moveTo(border, border); 518 | context.lineTo(border, height + border); 519 | context.moveTo(border, height + border); 520 | context.lineTo(width + border, height + border); 521 | context.stroke(); 522 | } 523 | 524 | /** 525 | * A measure of correlation between a terrain and its fitted plane. 526 | * 527 | * This uses a different approach than the common one (R^2, aka Pearson's 528 | * correlation coefficient) because the range is constricted and the data is 529 | * often non-normal. The approach taken here compares the differences between 530 | * the terrain elevations and the fitted plane at each vertex, and divides by 531 | * half the range to arrive at a dimensionless value. 532 | * 533 | * @param {Float32Array} vertices 534 | * The terrain vertex positions. 535 | * @param {THREE.Vector3} centroid 536 | * The fitted plane centroid. 537 | * @param {THREE.Vector3} normal 538 | * The fitted plane normal. 539 | * @param {Number} range 540 | * The allowed range in elevations. 541 | * 542 | * @return {Number} 543 | * Returns a number between 0 and 1 indicating how well the fitted plane 544 | * explains the variation in terrain elevation. 1 means entirely explained; 0 545 | * means not explained at all. 546 | */ 547 | function percentVariationExplainedByFittedPlane(vertices, centroid, normal, range) { 548 | var numVertices = vertices.length, 549 | diff = 0; 550 | for (var i = 0; i < numVertices; i += 3) { 551 | var fittedZ = Math.sqrt( 552 | (vertices[i + 0] - centroid.x) * (vertices[i + 0] - centroid.x) + 553 | (vertices[i + 1] - centroid.y) * (vertices[i + 1] - centroid.y) 554 | ) * Math.tan(normal.z * Math.PI) + centroid.z; 555 | diff += (vertices[i + 2] - fittedZ) * (vertices[i + 2] - fittedZ); 556 | } 557 | return 1 - Math.sqrt(diff / numVertices) * 2 / range; 558 | } 559 | 560 | function mean(data) { 561 | var sum = 0, 562 | l = data.length; 563 | for (var i = 0; i < l; i++) { 564 | sum += data[i]; 565 | } 566 | return sum / l; 567 | } 568 | 569 | })(); 570 | -------------------------------------------------------------------------------- /src/brownian.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate random terrain using Brownian motion. 3 | * 4 | * Note that this method takes a particularly long time to run (a few seconds). 5 | * 6 | * Parameters are the same as those for {@link THREE.Terrain.DiamondSquare}. 7 | */ 8 | THREE.Terrain.Brownian = function(g, options) { 9 | var untouched = [], 10 | touched = [], 11 | smallerSideSize = Math.min(options.xSize, options.ySize), 12 | changeDirectionProbability = Math.sqrt(smallerSideSize) / smallerSideSize, 13 | maxHeightAdjust = Math.sqrt(options.maxHeight - options.minHeight), 14 | xl = options.xSegments + 1, 15 | yl = options.ySegments + 1, 16 | i = Math.floor(Math.random() * options.xSegments), 17 | j = Math.floor(Math.random() * options.ySegments), 18 | x = i, 19 | y = j, 20 | numVertices = g.length, 21 | vertices = Array.from(g).map(function(z) { 22 | return { z: z }; 23 | }), 24 | current = vertices[j * xl + i], 25 | randomDirection = Math.random() * Math.PI * 2, 26 | addX = Math.cos(randomDirection), 27 | addY = Math.sin(randomDirection), 28 | n, 29 | m, 30 | key, 31 | sum, 32 | c, 33 | lastAdjust, 34 | index; 35 | 36 | // Initialize the first vertex. 37 | current.z = Math.random() * (options.maxHeight - options.minHeight) + options.minHeight; 38 | touched.push(current); 39 | 40 | // Walk through all vertices until they've all been adjusted. 41 | while (touched.length !== numVertices) { 42 | // Mark the untouched neighboring vertices to revisit later. 43 | for (n = -1; n <= 1; n++) { 44 | for (m = -1; m <= 1; m++) { 45 | key = (j+n)*xl + i + m; 46 | if (typeof vertices[key] !== 'undefined' && touched.indexOf(vertices[key]) === -1 && i+m >= 0 && j+n >= 0 && i+m < xl && j+n < yl && n && m) { 47 | untouched.push(vertices[key]); 48 | } 49 | } 50 | } 51 | 52 | // Occasionally, pick a random untouched point instead of continuing. 53 | if (Math.random() < changeDirectionProbability) { 54 | current = untouched.splice(Math.floor(Math.random() * untouched.length), 1)[0]; 55 | randomDirection = Math.random() * Math.PI * 2; 56 | addX = Math.cos(randomDirection); 57 | addY = Math.sin(randomDirection); 58 | index = vertices.indexOf(current); 59 | i = index % xl; 60 | j = Math.floor(index / xl); 61 | x = i; 62 | y = j; 63 | } 64 | else { 65 | // Keep walking in the current direction. 66 | var u = x, 67 | v = y; 68 | while (Math.round(u) === i && Math.round(v) === j) { 69 | u += addX; 70 | v += addY; 71 | } 72 | i = Math.round(u); 73 | j = Math.round(u); 74 | 75 | // If we hit a touched vertex, look in different directions to try to find an untouched one. 76 | for (var k = 0; i >= 0 && j >= 0 && i < xl && j < yl && touched.indexOf(vertices[j * xl + i]) !== -1 && k < 9; k++) { 77 | randomDirection = Math.random() * Math.PI * 2; 78 | addX = Math.cos(randomDirection); 79 | addY = Math.sin(randomDirection); 80 | while (Math.round(u) === i && Math.round(v) === j) { 81 | u += addX; 82 | v += addY; 83 | } 84 | i = Math.round(u); 85 | j = Math.round(v); 86 | } 87 | 88 | // If we found an untouched vertex, make it the current one. 89 | if (i >= 0 && j >= 0 && i < xl && j < yl && touched.indexOf(vertices[j * xl + i]) === -1) { 90 | x = u; 91 | y = v; 92 | current = vertices[j * xl + i]; 93 | var io = untouched.indexOf(current); 94 | if (io !== -1) { 95 | untouched.splice(io, 1); 96 | } 97 | } 98 | 99 | // If we couldn't find an untouched vertex near the current point, 100 | // pick a random untouched vertex instead. 101 | else { 102 | current = untouched.splice(Math.floor(Math.random() * untouched.length), 1)[0]; 103 | randomDirection = Math.random() * Math.PI * 2; 104 | addX = Math.cos(randomDirection); 105 | addY = Math.sin(randomDirection); 106 | index = vertices.indexOf(current); 107 | i = index % xl; 108 | j = Math.floor(index / xl); 109 | x = i; 110 | y = j; 111 | } 112 | } 113 | 114 | // Set the current vertex to the average elevation of its touched neighbors plus a random amount 115 | sum = 0; 116 | c = 0; 117 | for (n = -1; n <= 1; n++) { 118 | for (m = -1; m <= 1; m++) { 119 | key = (j+n)*xl + i + m; 120 | if (typeof vertices[key] !== 'undefined' && touched.indexOf(vertices[key]) !== -1 && i+m >= 0 && j+n >= 0 && i+m < xl && j+n < yl && n && m) { 121 | sum += vertices[key].z; 122 | c++; 123 | } 124 | } 125 | } 126 | if (c) { 127 | if (!lastAdjust || Math.random() < changeDirectionProbability) { 128 | lastAdjust = Math.random(); 129 | } 130 | current.z = sum / c + THREE.Terrain.EaseInWeak(lastAdjust) * maxHeightAdjust * 2 - maxHeightAdjust; 131 | } 132 | touched.push(current); 133 | } 134 | 135 | for (i = vertices.length - 1; i >= 0; i--) { 136 | g[i] = vertices[i].z; 137 | } 138 | 139 | // Erase artifacts. 140 | THREE.Terrain.Smooth(g, options); 141 | THREE.Terrain.Smooth(g, options); 142 | }; 143 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A terrain object for use with the Three.js library. 3 | * 4 | * Usage: `var terrainScene = THREE.Terrain();` 5 | * 6 | * @param {Object} [options] 7 | * An optional map of settings that control how the terrain is constructed 8 | * and displayed. Options include: 9 | * 10 | * - `after`: A function to run after other transformations on the terrain 11 | * produce the highest-detail heightmap, but before optimizations and 12 | * visual properties are applied. Takes two parameters, which are the same 13 | * as those for {@link THREE.Terrain.DiamondSquare}: an array of 14 | * `THREE.Vector3` objects representing the vertices of the terrain, and a 15 | * map of options with the same available properties as the `options` 16 | * parameter for the `THREE.Terrain` function. 17 | * - `easing`: A function that affects the distribution of slopes by 18 | * interpolating the height of each vertex along a curve. Valid values 19 | * include `THREE.Terrain.Linear` (the default), `THREE.Terrain.EaseIn`, 20 | * `THREE.Terrain.EaseOut`, `THREE.Terrain.EaseInOut`, 21 | * `THREE.Terrain.InEaseOut`, and any custom function that accepts a float 22 | * between 0 and 1 and returns a float between 0 and 1. 23 | * - `frequency`: For terrain generation methods that support it (Perlin, 24 | * Simplex, and Worley) the octave of randomness. This basically controls 25 | * how big features of the terrain will be (higher frequencies result in 26 | * smaller features). Often running multiple generation functions with 27 | * different frequencies and heights results in nice detail, as 28 | * the PerlinLayers and SimplexLayers methods demonstrate. (The counterpart 29 | * to frequency, amplitude, is represented by the difference between the 30 | * `maxHeight` and `minHeight` parameters.) Defaults to 2.5. 31 | * - `heightmap`: Either a canvas or pre-loaded image (from the same domain 32 | * as the webpage or served with a CORS-friendly header) representing 33 | * terrain height data (lighter pixels are higher); or a function used to 34 | * generate random height data for the terrain. Valid random functions are 35 | * specified in `generators.js` (or custom functions with the same 36 | * signature). Ideally heightmap images have the same number of pixels as 37 | * the terrain has vertices, as determined by the `xSegments` and 38 | * `ySegments` options, but this is not required. If the heightmap is a 39 | * different size, vertex height values will be interpolated.) Defaults to 40 | * `THREE.Terrain.DiamondSquare`. 41 | * - `material`: a THREE.Material instance used to display the terrain. 42 | * Defaults to `new THREE.MeshBasicMaterial({color: 0xee6633})`. 43 | * - `maxHeight`: the highest point, in Three.js units, that a peak should 44 | * reach. Defaults to 100. Setting to `undefined`, `null`, or `Infinity` 45 | * removes the cap, but this is generally not recommended because many 46 | * generators and filters require a vertical range. Instead, consider 47 | * setting the `stretch` option to `false`. 48 | * - `minHeight`: the lowest point, in Three.js units, that a valley should 49 | * reach. Defaults to -100. Setting to `undefined`, `null`, or `-Infinity` 50 | * removes the cap, but this is generally not recommended because many 51 | * generators and filters require a vertical range. Instead, consider 52 | * setting the `stretch` option to `false`. 53 | * - `steps`: If this is a number above 1, the terrain will be paritioned 54 | * into that many flat "steps," resulting in a blocky appearance. Defaults 55 | * to 1. 56 | * - `stretch`: Determines whether to stretch the heightmap across the 57 | * maximum and minimum height range if the height range produced by the 58 | * `heightmap` property is smaller. Defaults to true. 59 | * - `turbulent`: Whether to perform a turbulence transformation. Defaults to 60 | * false. 61 | * - `xSegments`: The number of segments (rows) to divide the terrain plane 62 | * into. (This basically determines how detailed the terrain is.) Defaults 63 | * to 63. 64 | * - `xSize`: The width of the terrain in Three.js units. Defaults to 1024. 65 | * Rendering might be slightly faster if this is a multiple of 66 | * `options.xSegments + 1`. 67 | * - `ySegments`: The number of segments (columns) to divide the terrain 68 | * plane into. (This basically determines how detailed the terrain is.) 69 | * Defaults to 63. 70 | * - `ySize`: The length of the terrain in Three.js units. Defaults to 1024. 71 | * Rendering might be slightly faster if this is a multiple of 72 | * `options.ySegments + 1`. 73 | */ 74 | THREE.Terrain = function(options) { 75 | var defaultOptions = { 76 | after: null, 77 | easing: THREE.Terrain.Linear, 78 | heightmap: THREE.Terrain.DiamondSquare, 79 | material: null, 80 | maxHeight: 100, 81 | minHeight: -100, 82 | optimization: THREE.Terrain.NONE, 83 | frequency: 2.5, 84 | steps: 1, 85 | stretch: true, 86 | turbulent: false, 87 | xSegments: 63, 88 | xSize: 1024, 89 | ySegments: 63, 90 | ySize: 1024, 91 | }; 92 | options = options || {}; 93 | for (var opt in defaultOptions) { 94 | if (defaultOptions.hasOwnProperty(opt)) { 95 | options[opt] = typeof options[opt] === 'undefined' ? defaultOptions[opt] : options[opt]; 96 | } 97 | } 98 | options.material = options.material || new THREE.MeshBasicMaterial({ color: 0xee6633 }); 99 | 100 | // Encapsulating the terrain in a parent object allows us the flexibility 101 | // to more easily have multiple meshes for optimization purposes. 102 | var scene = new THREE.Object3D(); 103 | // Planes are initialized on the XY plane, so rotate the plane to make it lie flat. 104 | scene.rotation.x = -0.5 * Math.PI; 105 | 106 | // Create the terrain mesh. 107 | var mesh = new THREE.Mesh( 108 | new THREE.PlaneGeometry(options.xSize, options.ySize, options.xSegments, options.ySegments), 109 | options.material 110 | ); 111 | 112 | // Assign elevation data to the terrain plane from a heightmap or function. 113 | var zs = THREE.Terrain.toArray1D(mesh.geometry.attributes.position.array); 114 | if (options.heightmap instanceof HTMLCanvasElement || options.heightmap instanceof Image) { 115 | THREE.Terrain.fromHeightmap(zs, options); 116 | } 117 | else if (typeof options.heightmap === 'function') { 118 | options.heightmap(zs, options); 119 | } 120 | else { 121 | console.warn('An invalid value was passed for `options.heightmap`: ' + options.heightmap); 122 | } 123 | THREE.Terrain.fromArray1D(mesh.geometry.attributes.position.array, zs); 124 | THREE.Terrain.Normalize(mesh, options); 125 | 126 | // lod.addLevel(mesh, options.unit * 10 * Math.pow(2, lodLevel)); 127 | 128 | scene.add(mesh); 129 | return scene; 130 | }; 131 | 132 | /** 133 | * Normalize the terrain after applying a heightmap or filter. 134 | * 135 | * This applies turbulence, steps, and height clamping; calls the `after` 136 | * callback; updates normals and the bounding sphere; and marks vertices as 137 | * dirty. 138 | * 139 | * @param {THREE.Mesh} mesh 140 | * The terrain mesh. 141 | * @param {Object} options 142 | * A map of settings that control how the terrain is constructed and 143 | * displayed. Valid options are the same as for {@link THREE.Terrain}(). 144 | */ 145 | THREE.Terrain.Normalize = function(mesh, options) { 146 | var zs = THREE.Terrain.toArray1D(mesh.geometry.attributes.position.array); 147 | if (options.turbulent) { 148 | THREE.Terrain.Turbulence(zs, options); 149 | } 150 | if (options.steps > 1) { 151 | THREE.Terrain.Step(zs, options.steps); 152 | THREE.Terrain.Smooth(zs, options); 153 | } 154 | 155 | // Keep the terrain within the allotted height range if necessary, and do easing. 156 | THREE.Terrain.Clamp(zs, options); 157 | 158 | // Call the "after" callback 159 | if (typeof options.after === 'function') { 160 | options.after(zs, options); 161 | } 162 | THREE.Terrain.fromArray1D(mesh.geometry.attributes.position.array, zs); 163 | 164 | // Mark the geometry as having changed and needing updates. 165 | mesh.geometry.computeBoundingSphere(); 166 | mesh.geometry.computeFaceNormals(); 167 | mesh.geometry.computeVertexNormals(); 168 | }; 169 | 170 | /** 171 | * Optimization types. 172 | * 173 | * Note that none of these are implemented right now. They should be done as 174 | * shaders so that they execute on the GPU, and the resulting scene would need 175 | * to be updated every frame to adjust to the camera's position. 176 | * 177 | * Further reading: 178 | * - http://vterrain.org/LOD/Papers/ 179 | * - http://vterrain.org/LOD/Implementations/ 180 | * 181 | * GEOMIPMAP: The terrain plane should be split into sections, each with their 182 | * own LODs, for screen-space occlusion and detail reduction. Intermediate 183 | * vertices on higher-detail neighboring sections should be interpolated 184 | * between neighbor edge vertices in order to match with the edge of the 185 | * lower-detail section. The number of sections should be around sqrt(segments) 186 | * along each axis. It's unclear how to make materials stretch across segments. 187 | * Possible example (I haven't looked too much into it) at 188 | * https://github.com/felixpalmer/lod-terrain/tree/master/js/shaders 189 | * 190 | * GEOCLIPMAP: The terrain should be composed of multiple donut-shaped sections 191 | * at decreasing resolution as the radius gets bigger. When the player moves, 192 | * the sections should morph so that the detail "follows" the player around. 193 | * There is an implementation of geoclipmapping at 194 | * https://github.com/CodeArtemis/TriggerRally/blob/unified/server/public/scripts/client/terrain.coffee 195 | * and a tutorial on morph targets at 196 | * http://nikdudnik.com/making-3d-gfx-for-the-cinema-on-low-budget-and-three-js/ 197 | * 198 | * POLYGONREDUCTION: Combine areas that are relatively coplanar into larger 199 | * polygons as described at http://www.shamusyoung.com/twentysidedtale/?p=142. 200 | * This method can be combined with the others if done very carefully, or it 201 | * can be adjusted to be more aggressive at greater distance from the camera 202 | * (similar to combining with geomipmapping). 203 | * 204 | * If these do get implemented, here is the option description to add to the 205 | * `THREE.Terrain` docblock: 206 | * 207 | * - `optimization`: the type of optimization to apply to the terrain. If 208 | * an optimization is applied, the number of segments along each axis that 209 | * the terrain should be divided into at the most detailed level should 210 | * equal (n * 2^(LODs-1))^2 - 1, for arbitrary n, where LODs is the number 211 | * of levels of detail desired. Valid values include: 212 | * 213 | * - `THREE.Terrain.NONE`: Don't apply any optimizations. This is the 214 | * default. 215 | * - `THREE.Terrain.GEOMIPMAP`: Divide the terrain into evenly-sized 216 | * sections with multiple levels of detail. For each section, 217 | * display a level of detail dependent on how close the camera is. 218 | * - `THREE.Terrain.GEOCLIPMAP`: Divide the terrain into donut-shaped 219 | * sections, where detail decreases as the radius increases. The 220 | * rings then morph to "follow" the camera around so that the camera 221 | * is always at the center, surrounded by the most detail. 222 | */ 223 | THREE.Terrain.NONE = 0; 224 | THREE.Terrain.GEOMIPMAP = 1; 225 | THREE.Terrain.GEOCLIPMAP = 2; 226 | THREE.Terrain.POLYGONREDUCTION = 3; 227 | 228 | /** 229 | * Get a 2D array of heightmap values from a 1D array of Z-positions. 230 | * 231 | * @param {Float32Array} vertices 232 | * A 1D array containing the vertex Z-positions of the geometry representing 233 | * the terrain. 234 | * @param {Object} options 235 | * A map of settings defining properties of the terrain. The only properties 236 | * that matter here are `xSegments` and `ySegments`, which represent how many 237 | * vertices wide and deep the terrain plane is, respectively (and therefore 238 | * also the dimensions of the returned array). 239 | * 240 | * @return {Float32Array[]} 241 | * A 2D array representing the terrain's heightmap. 242 | */ 243 | THREE.Terrain.toArray2D = function(vertices, options) { 244 | var tgt = new Array(options.xSegments + 1), 245 | xl = options.xSegments + 1, 246 | yl = options.ySegments + 1, 247 | i, j; 248 | for (i = 0; i < xl; i++) { 249 | tgt[i] = new Float32Array(options.ySegments + 1); 250 | for (j = 0; j < yl; j++) { 251 | tgt[i][j] = vertices[j * xl + i]; 252 | } 253 | } 254 | return tgt; 255 | }; 256 | 257 | /** 258 | * Set the height of plane vertices from a 2D array of heightmap values. 259 | * 260 | * @param {Float32Array} vertices 261 | * A 1D array containing the vertex Z-positions of the geometry representing 262 | * the terrain. 263 | * @param {Number[][]} src 264 | * A 2D array representing a heightmap to apply to the terrain. 265 | */ 266 | THREE.Terrain.fromArray2D = function(vertices, src) { 267 | for (var i = 0, xl = src.length; i < xl; i++) { 268 | for (var j = 0, yl = src[i].length; j < yl; j++) { 269 | vertices[j * xl + i] = src[i][j]; 270 | } 271 | } 272 | }; 273 | 274 | /** 275 | * Get a 1D array of heightmap values from a 1D array of plane vertices. 276 | * 277 | * @param {Float32Array} vertices 278 | * A 1D array containing the vertex positions of the geometry representing the 279 | * terrain. 280 | * @param {Object} options 281 | * A map of settings defining properties of the terrain. The only properties 282 | * that matter here are `xSegments` and `ySegments`, which represent how many 283 | * vertices wide and deep the terrain plane is, respectively (and therefore 284 | * also the dimensions of the returned array). 285 | * 286 | * @return {Float32Array} 287 | * A 1D array representing the terrain's heightmap. 288 | */ 289 | THREE.Terrain.toArray1D = function(vertices) { 290 | var tgt = new Float32Array(vertices.length / 3); 291 | for (var i = 0, l = tgt.length; i < l; i++) { 292 | tgt[i] = vertices[i * 3 + 2]; 293 | } 294 | return tgt; 295 | }; 296 | 297 | /** 298 | * Set the height of plane vertices from a 1D array of heightmap values. 299 | * 300 | * @param {Float32Array} vertices 301 | * A 1D array containing the vertex positions of the geometry representing the 302 | * terrain. 303 | * @param {Number[]} src 304 | * A 1D array representing a heightmap to apply to the terrain. 305 | */ 306 | THREE.Terrain.fromArray1D = function(vertices, src) { 307 | for (var i = 0, l = Math.min(vertices.length / 3, src.length); i < l; i++) { 308 | vertices[i * 3 + 2] = src[i]; 309 | } 310 | }; 311 | 312 | /** 313 | * Generate a 1D array containing random heightmap data. 314 | * 315 | * This is like {@link THREE.Terrain.toHeightmap} except that instead of 316 | * generating the Three.js mesh and material information you can just get the 317 | * height data. 318 | * 319 | * @param {Function} method 320 | * The method to use to generate the heightmap data. Works with function that 321 | * would be an acceptable value for the `heightmap` option for the 322 | * {@link THREE.Terrain} function. 323 | * @param {Number} options 324 | * The same as the options parameter for the {@link THREE.Terrain} function. 325 | */ 326 | THREE.Terrain.heightmapArray = function(method, options) { 327 | var arr = new Array((options.xSegments+1) * (options.ySegments+1)), 328 | l = arr.length, 329 | i; 330 | arr.fill(0); 331 | options.minHeight = options.minHeight || 0; 332 | options.maxHeight = typeof options.maxHeight === 'undefined' ? 1 : options.maxHeight; 333 | options.stretch = options.stretch || false; 334 | method(arr, options); 335 | THREE.Terrain.Clamp(arr, options); 336 | return arr; 337 | }; 338 | 339 | /** 340 | * Randomness interpolation functions. 341 | */ 342 | THREE.Terrain.Linear = function(x) { 343 | return x; 344 | }; 345 | 346 | // x = [0, 1], x^2 347 | THREE.Terrain.EaseIn = function(x) { 348 | return x*x; 349 | }; 350 | 351 | // x = [0, 1], -x(x-2) 352 | THREE.Terrain.EaseOut = function(x) { 353 | return -x * (x - 2); 354 | }; 355 | 356 | // x = [0, 1], x^2(3-2x) 357 | // Nearly identical alternatives: 0.5+0.5*cos(x*pi-pi), x^a/(x^a+(1-x)^a) (where a=1.6 seems nice) 358 | // For comparison: http://www.wolframalpha.com/input/?i=x^1.6%2F%28x^1.6%2B%281-x%29^1.6%29%2C+x^2%283-2x%29%2C+0.5%2B0.5*cos%28x*pi-pi%29+from+0+to+1 359 | THREE.Terrain.EaseInOut = function(x) { 360 | return x*x*(3-2*x); 361 | }; 362 | 363 | // x = [0, 1], 0.5*(2x-1)^3+0.5 364 | THREE.Terrain.InEaseOut = function(x) { 365 | var y = 2*x-1; 366 | return 0.5 * y*y*y + 0.5; 367 | }; 368 | 369 | // x = [0, 1], x^1.55 370 | THREE.Terrain.EaseInWeak = function(x) { 371 | return Math.pow(x, 1.55); 372 | }; 373 | 374 | // x = [0, 1], x^7 375 | THREE.Terrain.EaseInStrong = function(x) { 376 | return x*x*x*x*x*x*x; 377 | }; 378 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rescale the heightmap of a terrain to keep it within the maximum range. 3 | * 4 | * @param {Float32Array} g 5 | * The geometry's z-positions to modify with heightmap data. 6 | * @param {Object} options 7 | * A map of settings that control how the terrain is constructed and 8 | * displayed. Valid values are the same as those for the `options` parameter 9 | * of {@link THREE.Terrain}() but only `maxHeight`, `minHeight`, and `easing` 10 | * are used. 11 | */ 12 | THREE.Terrain.Clamp = function(g, options) { 13 | var min = Infinity, 14 | max = -Infinity, 15 | l = g.length, 16 | i; 17 | options.easing = options.easing || THREE.Terrain.Linear; 18 | for (i = 0; i < l; i++) { 19 | if (g[i] < min) min = g[i]; 20 | if (g[i] > max) max = g[i]; 21 | } 22 | var actualRange = max - min, 23 | optMax = typeof options.maxHeight !== 'number' ? max : options.maxHeight, 24 | optMin = typeof options.minHeight !== 'number' ? min : options.minHeight, 25 | targetMax = options.stretch ? optMax : (max < optMax ? max : optMax), 26 | targetMin = options.stretch ? optMin : (min > optMin ? min : optMin), 27 | range = targetMax - targetMin; 28 | if (targetMax < targetMin) { 29 | targetMax = optMax; 30 | range = targetMax - targetMin; 31 | } 32 | for (i = 0; i < l; i++) { 33 | g[i] = options.easing((g[i] - min) / actualRange) * range + optMin; 34 | } 35 | }; 36 | 37 | /** 38 | * Move the edges of the terrain up or down based on distance from the edge. 39 | * 40 | * Useful to make islands or enclosing walls/cliffs. 41 | * 42 | * @param {Float32Array} g 43 | * The geometry's z-positions to modify with heightmap data. 44 | * @param {Object} options 45 | * A map of settings that control how the terrain is constructed and 46 | * displayed. Valid values are the same as those for the `options` parameter 47 | * of {@link THREE.Terrain}(). 48 | * @param {Boolean} direction 49 | * `true` if the edges should be turned up; `false` if they should be turned 50 | * down. 51 | * @param {Number} distance 52 | * The distance from the edge at which the edges should begin to be affected 53 | * by this operation. 54 | * @param {Number/Function} [e=THREE.Terrain.EaseInOut] 55 | * A function that determines how quickly the terrain will transition between 56 | * its current height and the edge shape as distance to the edge decreases. 57 | * It does this by interpolating the height of each vertex along a curve. 58 | * Valid values include `THREE.Terrain.Linear`, `THREE.Terrain.EaseIn`, 59 | * `THREE.Terrain.EaseOut`, `THREE.Terrain.EaseInOut`, 60 | * `THREE.Terrain.InEaseOut`, and any custom function that accepts a float 61 | * between 0 and 1 and returns a float between 0 and 1. 62 | * @param {Object} [edges={top: true, bottom: true, left: true, right: true}] 63 | * Determines which edges should be affected by this function. Defaults to 64 | * all edges. If passed, should be an object with `top`, `bottom`, `left`, 65 | * and `right` Boolean properties specifying which edges to affect. 66 | */ 67 | THREE.Terrain.Edges = function(g, options, direction, distance, easing, edges) { 68 | var numXSegments = Math.floor(distance / (options.xSize / options.xSegments)) || 1, 69 | numYSegments = Math.floor(distance / (options.ySize / options.ySegments)) || 1, 70 | peak = direction ? options.maxHeight : options.minHeight, 71 | max = direction ? Math.max : Math.min, 72 | xl = options.xSegments + 1, 73 | yl = options.ySegments + 1, 74 | i, j, multiplier, k1, k2; 75 | easing = easing || THREE.Terrain.EaseInOut; 76 | if (typeof edges !== 'object') { 77 | edges = {top: true, bottom: true, left: true, right: true}; 78 | } 79 | for (i = 0; i < xl; i++) { 80 | for (j = 0; j < numYSegments; j++) { 81 | multiplier = easing(1 - j / numYSegments); 82 | k1 = j*xl + i; 83 | k2 = (options.ySegments-j)*xl + i; 84 | if (edges.top) { 85 | g[k1] = max(g[k1], (peak - g[k1]) * multiplier + g[k1]); 86 | } 87 | if (edges.bottom) { 88 | g[k2] = max(g[k2], (peak - g[k2]) * multiplier + g[k2]); 89 | } 90 | } 91 | } 92 | for (i = 0; i < yl; i++) { 93 | for (j = 0; j < numXSegments; j++) { 94 | multiplier = easing(1 - j / numXSegments); 95 | k1 = i*xl+j; 96 | k2 = (options.ySegments-i)*xl + (options.xSegments-j); 97 | if (edges.left) { 98 | g[k1] = max(g[k1], (peak - g[k1]) * multiplier + g[k1]); 99 | } 100 | if (edges.right) { 101 | g[k2] = max(g[k2], (peak - g[k2]) * multiplier + g[k2]); 102 | } 103 | } 104 | } 105 | THREE.Terrain.Clamp(g, { 106 | maxHeight: options.maxHeight, 107 | minHeight: options.minHeight, 108 | stretch: true, 109 | }); 110 | }; 111 | 112 | /** 113 | * Move the edges of the terrain up or down based on distance from the center. 114 | * 115 | * Useful to make islands or enclosing walls/cliffs. 116 | * 117 | * @param {Float32Array} g 118 | * The geometry's z-positions to modify with heightmap data. 119 | * @param {Object} options 120 | * A map of settings that control how the terrain is constructed and 121 | * displayed. Valid values are the same as those for the `options` parameter 122 | * of {@link THREE.Terrain}(). 123 | * @param {Boolean} direction 124 | * `true` if the edges should be turned up; `false` if they should be turned 125 | * down. 126 | * @param {Number} distance 127 | * The distance from the center at which the edges should begin to be 128 | * affected by this operation. 129 | * @param {Number/Function} [e=THREE.Terrain.EaseInOut] 130 | * A function that determines how quickly the terrain will transition between 131 | * its current height and the edge shape as distance to the edge decreases. 132 | * It does this by interpolating the height of each vertex along a curve. 133 | * Valid values include `THREE.Terrain.Linear`, `THREE.Terrain.EaseIn`, 134 | * `THREE.Terrain.EaseOut`, `THREE.Terrain.EaseInOut`, 135 | * `THREE.Terrain.InEaseOut`, and any custom function that accepts a float 136 | * between 0 and 1 and returns a float between 0 and 1. 137 | */ 138 | THREE.Terrain.RadialEdges = function(g, options, direction, distance, easing) { 139 | var peak = direction ? options.maxHeight : options.minHeight, 140 | max = direction ? Math.max : Math.min, 141 | xl = (options.xSegments + 1), 142 | yl = (options.ySegments + 1), 143 | xl2 = xl * 0.5, 144 | yl2 = yl * 0.5, 145 | xSegmentSize = options.xSize / options.xSegments, 146 | ySegmentSize = options.ySize / options.ySegments, 147 | edgeRadius = Math.min(options.xSize, options.ySize) * 0.5 - distance, 148 | i, j, multiplier, k, vertexDistance; 149 | for (i = 0; i < xl; i++) { 150 | for (j = 0; j < yl2; j++) { 151 | k = j*xl + i; 152 | vertexDistance = Math.min(edgeRadius, Math.sqrt((xl2-i)*xSegmentSize*(xl2-i)*xSegmentSize + (yl2-j)*ySegmentSize*(yl2-j)*ySegmentSize) - distance); 153 | if (vertexDistance < 0) continue; 154 | multiplier = easing(vertexDistance / edgeRadius); 155 | g[k] = max(g[k], (peak - g[k]) * multiplier + g[k]); 156 | // Use symmetry to reduce the number of iterations. 157 | k = (options.ySegments-j)*xl + i; 158 | g[k] = max(g[k], (peak - g[k]) * multiplier + g[k]); 159 | } 160 | } 161 | }; 162 | 163 | /** 164 | * Smooth the terrain by setting each point to the mean of its neighborhood. 165 | * 166 | * @param {Float32Array} g 167 | * The geometry's z-positions to modify with heightmap data. 168 | * @param {Object} options 169 | * A map of settings that control how the terrain is constructed and 170 | * displayed. Valid values are the same as those for the `options` parameter 171 | * of {@link THREE.Terrain}(). 172 | * @param {Number} [weight=0] 173 | * How much to weight the original vertex height against the average of its 174 | * neighbors. 175 | */ 176 | THREE.Terrain.Smooth = function(g, options, weight) { 177 | var heightmap = new Float32Array(g.length); 178 | for (var i = 0, xl = options.xSegments + 1, yl = options.ySegments + 1; i < xl; i++) { 179 | for (var j = 0; j < yl; j++) { 180 | var sum = 0, 181 | c = 0; 182 | for (var n = -1; n <= 1; n++) { 183 | for (var m = -1; m <= 1; m++) { 184 | var key = (j+n)*xl + i + m; 185 | if (typeof g[key] !== 'undefined' && i+m >= 0 && j+n >= 0 && i+m < xl && j+n < yl) { 186 | sum += g[key]; 187 | c++; 188 | } 189 | } 190 | } 191 | heightmap[j*xl + i] = sum / c; 192 | } 193 | } 194 | weight = weight || 0; 195 | var w = 1 / (1 + weight); 196 | for (var k = 0, l = g.length; k < l; k++) { 197 | g[k] = (heightmap[k] + g[k] * weight) * w; 198 | } 199 | }; 200 | 201 | /** 202 | * Smooth the terrain by setting each point to the median of its neighborhood. 203 | * 204 | * @param {Float32Array} g 205 | * The geometry's z-positions to modify with heightmap data. 206 | * @param {Object} options 207 | * A map of settings that control how the terrain is constructed and 208 | * displayed. Valid values are the same as those for the `options` parameter 209 | * of {@link THREE.Terrain}(). 210 | */ 211 | THREE.Terrain.SmoothMedian = function(g, options) { 212 | var heightmap = new Float32Array(g.length), 213 | neighborValues = [], 214 | neighborKeys = [], 215 | sortByValue = function(a, b) { 216 | return neighborValues[a] - neighborValues[b]; 217 | }; 218 | for (var i = 0, xl = options.xSegments + 1, yl = options.ySegments + 1; i < xl; i++) { 219 | for (var j = 0; j < yl; j++) { 220 | neighborValues.length = 0; 221 | neighborKeys.length = 0; 222 | for (var n = -1; n <= 1; n++) { 223 | for (var m = -1; m <= 1; m++) { 224 | var key = (j+n)*xl + i + m; 225 | if (typeof g[key] !== 'undefined' && i+m >= 0 && j+n >= 0 && i+m < xl && j+n < yl) { 226 | neighborValues.push(g[key]); 227 | neighborKeys.push(key); 228 | } 229 | } 230 | } 231 | neighborKeys.sort(sortByValue); 232 | var halfKey = Math.floor(neighborKeys.length*0.5), 233 | median; 234 | if (neighborKeys.length % 2 === 1) { 235 | median = g[neighborKeys[halfKey]]; 236 | } 237 | else { 238 | median = (g[neighborKeys[halfKey-1]] + g[neighborKeys[halfKey]]) * 0.5; 239 | } 240 | heightmap[j*xl + i] = median; 241 | } 242 | } 243 | for (var k = 0, l = g.length; k < l; k++) { 244 | g[k] = heightmap[k]; 245 | } 246 | }; 247 | 248 | /** 249 | * Smooth the terrain by clamping each point within its neighbors' extremes. 250 | * 251 | * @param {Float32Array} g 252 | * The geometry's z-positions to modify with heightmap data. 253 | * @param {Object} options 254 | * A map of settings that control how the terrain is constructed and 255 | * displayed. Valid values are the same as those for the `options` parameter 256 | * of {@link THREE.Terrain}(). 257 | * @param {Number} [multiplier=1] 258 | * By default, this filter clamps each point within the highest and lowest 259 | * value of its neighbors. This parameter is a multiplier for the range 260 | * outside of which the point will be clamped. Higher values mean that the 261 | * point can be farther outside the range of its neighbors. 262 | */ 263 | THREE.Terrain.SmoothConservative = function(g, options, multiplier) { 264 | var heightmap = new Float32Array(g.length); 265 | for (var i = 0, xl = options.xSegments + 1, yl = options.ySegments + 1; i < xl; i++) { 266 | for (var j = 0; j < yl; j++) { 267 | var max = -Infinity, 268 | min = Infinity; 269 | for (var n = -1; n <= 1; n++) { 270 | for (var m = -1; m <= 1; m++) { 271 | var key = (j+n)*xl + i + m; 272 | if (typeof g[key] !== 'undefined' && n && m && i+m >= 0 && j+n >= 0 && i+m < xl && j+n < yl) { 273 | if (g[key] < min) min = g[key]; 274 | if (g[key] > max) max = g[key]; 275 | } 276 | } 277 | } 278 | var kk = j*xl + i; 279 | if (typeof multiplier === 'number') { 280 | var halfdiff = (max - min) * 0.5, 281 | middle = min + halfdiff; 282 | max = middle + halfdiff * multiplier; 283 | min = middle - halfdiff * multiplier; 284 | } 285 | heightmap[kk] = g[kk] > max ? max : (g[kk] < min ? min : g[kk]); 286 | } 287 | } 288 | for (var k = 0, l = g.length; k < l; k++) { 289 | g[k] = heightmap[k]; 290 | } 291 | }; 292 | 293 | /** 294 | * Partition a terrain into flat steps. 295 | * 296 | * @param {Float32Array} g 297 | * The geometry's z-positions to modify with heightmap data. 298 | * @param {Number} [levels] 299 | * The number of steps to divide the terrain into. Defaults to 300 | * (g.length/2)^(1/4). 301 | */ 302 | THREE.Terrain.Step = function(g, levels) { 303 | // Calculate the max, min, and avg values for each bucket 304 | var i = 0, 305 | j = 0, 306 | l = g.length, 307 | inc = Math.floor(l / levels), 308 | heights = new Array(l), 309 | buckets = new Array(levels); 310 | if (typeof levels === 'undefined') { 311 | levels = Math.floor(Math.pow(l*0.5, 0.25)); 312 | } 313 | for (i = 0; i < l; i++) { 314 | heights[i] = g[i]; 315 | } 316 | heights.sort(function(a, b) { return a - b; }); 317 | for (i = 0; i < levels; i++) { 318 | // Bucket by population (bucket size) not range size 319 | var subset = heights.slice(i*inc, (i+1)*inc), 320 | sum = 0, 321 | bl = subset.length; 322 | for (j = 0; j < bl; j++) { 323 | sum += subset[j]; 324 | } 325 | buckets[i] = { 326 | min: subset[0], 327 | max: subset[subset.length-1], 328 | avg: sum / bl, 329 | }; 330 | } 331 | 332 | // Set the height of each vertex to the average height of its bucket 333 | for (i = 0; i < l; i++) { 334 | var startHeight = g[i]; 335 | for (j = 0; j < levels; j++) { 336 | if (startHeight >= buckets[j].min && startHeight <= buckets[j].max) { 337 | g[i] = buckets[j].avg; 338 | break; 339 | } 340 | } 341 | } 342 | }; 343 | 344 | /** 345 | * Transform to turbulent noise. 346 | * 347 | * @param {Float32Array} g 348 | * The geometry's z-positions to modify with heightmap data. 349 | * @param {Object} [options] 350 | * The same map of settings you'd pass to {@link THREE.Terrain()}. Only 351 | * `minHeight` and `maxHeight` are used (and required) here. 352 | */ 353 | THREE.Terrain.Turbulence = function(g, options) { 354 | var range = options.maxHeight - options.minHeight; 355 | for (var i = 0, l = g.length; i < l; i++) { 356 | g[i] = options.minHeight + Math.abs((g[i] - options.minHeight) * 2 - range); 357 | } 358 | }; 359 | -------------------------------------------------------------------------------- /src/gaussian.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | /** 4 | * Convolve an array with a kernel. 5 | * 6 | * @param {Number[][]} src 7 | * The source array to convolve. A nonzero-sized rectangular array of numbers. 8 | * @param {Number[][]} kernel 9 | * The kernel array with which to convolve `src`. A nonzero-sized 10 | * rectangular array of numbers smaller than `src`. 11 | * @param {Number[][]} [tgt] 12 | * The target array into which the result of the convolution should be put. 13 | * If not passed, a new array will be created. This is also the array that 14 | * this function returns. It must be at least as large as `src`. 15 | * 16 | * @return {Number[][]} 17 | * An array containing the result of the convolution. 18 | */ 19 | function convolve(src, kernel, tgt) { 20 | // src and kernel must be nonzero rectangular number arrays. 21 | if (!src.length || !kernel.length) return src; 22 | // Initialize tracking variables. 23 | var i = 0, // current src x-position 24 | j = 0, // current src y-position 25 | a = 0, // current kernel x-position 26 | b = 0, // current kernel y-position 27 | w = src.length, // src width 28 | l = src[0].length, // src length 29 | m = kernel.length, // kernel width 30 | n = kernel[0].length; // kernel length 31 | // If a target isn't passed, initialize it to an array the same size as src. 32 | if (typeof tgt === 'undefined') { 33 | tgt = new Array(w); 34 | for (i = 0; i < w; i++) { 35 | tgt[i] = new Float64Array(l); 36 | } 37 | } 38 | // The kernel is a rectangle smaller than the source. Hold it over the 39 | // source so that its top-left value sits over the target position. Then, 40 | // for each value in the kernel, multiply it by the value in the source 41 | // that it is sitting on top of. The target value at that position is the 42 | // sum of those products. 43 | // For each position in the source: 44 | for (i = 0; i < w; i++) { 45 | for (j = 0; j < l; j++) { 46 | var last = 0; 47 | tgt[i][j] = 0; 48 | // For each position in the kernel: 49 | for (a = 0; a < m; a++) { 50 | for (b = 0; b < n; b++) { 51 | // If we're along the right or bottom edges of the source, 52 | // parts of the kernel will fall outside of the source. In 53 | // that case, pretend the source value is the last valid 54 | // value we got from the source. This gives reasonable 55 | // results. The alternative is to drop the edges and end up 56 | // with a target smaller than the source. That is 57 | // unreasonable for some applications, so we let the caller 58 | // make that choice. 59 | if (typeof src[i+a] !== 'undefined' && 60 | typeof src[i+a][j+b] !== 'undefined') { 61 | last = src[i+a][j+b]; 62 | } 63 | // Multiply the source and the kernel at this position. 64 | // The value at the target position is the sum of these 65 | // products. 66 | tgt[i][j] += last * kernel[a][b]; 67 | } 68 | } 69 | } 70 | } 71 | return tgt; 72 | } 73 | 74 | /** 75 | * Returns the value at X of a Gaussian distribution with standard deviation S. 76 | */ 77 | function gauss(x, s) { 78 | // 2.5066282746310005 is sqrt(2*pi) 79 | return Math.exp(-0.5 * x*x / (s*s)) / (s * 2.5066282746310005); 80 | } 81 | 82 | /** 83 | * Generate a Gaussian kernel. 84 | * 85 | * Returns a kernel of size N approximating a 1D Gaussian distribution with 86 | * standard deviation S. 87 | */ 88 | function gaussianKernel1D(s, n) { 89 | if (typeof n !== 'number') n = 7; 90 | var kernel = new Float64Array(n), 91 | halfN = Math.floor(n * 0.5), 92 | odd = n % 2, 93 | i; 94 | if (!s || !n) return kernel; 95 | for (i = 0; i <= halfN; i++) { 96 | kernel[i] = gauss(s * (i - halfN - odd * 0.5), s); 97 | } 98 | for (; i < n; i++) { 99 | kernel[i] = kernel[n - 1 - i]; 100 | } 101 | return kernel; 102 | } 103 | 104 | /** 105 | * Perform Gaussian smoothing. 106 | * 107 | * @param {Number[][]} src 108 | * The source array to convolve. A nonzero-sized rectangular array of numbers. 109 | * @param {Number} [s=1] 110 | * The standard deviation of the Gaussian kernel to use. Higher values result 111 | * in smoothing across more cells of the src matrix. 112 | * @param {Number} [kernelSize=7] 113 | * The size of the Gaussian kernel to use. Larger kernels result in slower 114 | * but more accurate smoothing. 115 | * 116 | * @return {Number[][]} 117 | * An array containing the result of smoothing the src. 118 | */ 119 | function gaussian(src, s, kernelSize) { 120 | if (typeof s === 'undefined') s = 1; 121 | if (typeof kernelSize === 'undefined') kernelSize = 7; 122 | var kernel = gaussianKernel1D(s, kernelSize), 123 | l = kernelSize || kernel.length, 124 | kernelH = [kernel], 125 | kernelV = new Array(l); 126 | for (var i = 0; i < l; i++) { 127 | kernelV[i] = [kernel[i]]; 128 | } 129 | return convolve(convolve(src, kernelH), kernelV); 130 | } 131 | 132 | /** 133 | * Perform Gaussian smoothing on terrain vertices. 134 | * 135 | * @param {THREE.Vector3[]} g 136 | * The vertex array for plane geometry to modify with heightmap data. This 137 | * method sets the `z` property of each vertex. 138 | * @param {Object} options 139 | * A map of settings that control how the terrain is constructed and 140 | * displayed. Valid values are the same as those for the `options` parameter 141 | * of {@link THREE.Terrain}(). 142 | * @param {Number} [s=1] 143 | * The standard deviation of the Gaussian kernel to use. Higher values result 144 | * in smoothing across more cells of the src matrix. 145 | * @param {Number} [kernelSize=7] 146 | * The size of the Gaussian kernel to use. Larger kernels result in slower 147 | * but more accurate smoothing. 148 | */ 149 | THREE.Terrain.Gaussian = function(g, options, s, kernelSize) { 150 | THREE.Terrain.fromArray2D(g, gaussian(THREE.Terrain.toArray2D(g, options), s, kernelSize)); 151 | }; 152 | 153 | })(); 154 | -------------------------------------------------------------------------------- /src/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert an image-based heightmap into vertex-based height data. 3 | * 4 | * @param {Float32Array} g 5 | * The geometry's z-positions to modify with heightmap data. 6 | * @param {Object} options 7 | * A map of settings that control how the terrain is constructed and 8 | * displayed. Valid values are the same as those for the `options` parameter 9 | * of {@link THREE.Terrain}(). 10 | */ 11 | THREE.Terrain.fromHeightmap = function(g, options) { 12 | var canvas = document.createElement('canvas'), 13 | context = canvas.getContext('2d'), 14 | rows = options.ySegments + 1, 15 | cols = options.xSegments + 1, 16 | spread = options.maxHeight - options.minHeight; 17 | canvas.width = cols; 18 | canvas.height = rows; 19 | context.drawImage(options.heightmap, 0, 0, canvas.width, canvas.height); 20 | var data = context.getImageData(0, 0, canvas.width, canvas.height).data; 21 | for (var row = 0; row < rows; row++) { 22 | for (var col = 0; col < cols; col++) { 23 | var i = row * cols + col, 24 | idx = i * 4; 25 | g[i] = (data[idx] + data[idx+1] + data[idx+2]) / 765 * spread + options.minHeight; 26 | } 27 | } 28 | }; 29 | 30 | /** 31 | * Convert a terrain plane into an image-based heightmap. 32 | * 33 | * Parameters are the same as for {@link THREE.Terrain.fromHeightmap} except 34 | * that if `options.heightmap` is a canvas element then the image will be 35 | * painted onto that canvas; otherwise a new canvas will be created. 36 | * 37 | * @param {Float32Array} g 38 | * The vertex position array for the geometry to paint to a heightmap. 39 | * @param {Object} options 40 | * A map of settings that control how the terrain is constructed and 41 | * displayed. Valid values are the same as those for the `options` parameter 42 | * of {@link THREE.Terrain}(). 43 | * 44 | * @return {HTMLCanvasElement} 45 | * A canvas with the relevant heightmap painted on it. 46 | */ 47 | THREE.Terrain.toHeightmap = function(g, options) { 48 | var hasMax = typeof options.maxHeight !== 'undefined', 49 | hasMin = typeof options.minHeight !== 'undefined', 50 | max = hasMax ? options.maxHeight : -Infinity, 51 | min = hasMin ? options.minHeight : Infinity; 52 | if (!hasMax || !hasMin) { 53 | var max2 = max, 54 | min2 = min; 55 | for (var k = 2, l = g.length; k < l; k += 3) { 56 | if (g[k] > max2) max2 = g[k]; 57 | if (g[k] < min2) min2 = g[k]; 58 | } 59 | if (!hasMax) max = max2; 60 | if (!hasMin) min = min2; 61 | } 62 | var canvas = options.heightmap instanceof HTMLCanvasElement ? options.heightmap : document.createElement('canvas'), 63 | context = canvas.getContext('2d'), 64 | rows = options.ySegments + 1, 65 | cols = options.xSegments + 1, 66 | spread = max - min; 67 | canvas.width = cols; 68 | canvas.height = rows; 69 | var d = context.createImageData(canvas.width, canvas.height), 70 | data = d.data; 71 | for (var row = 0; row < rows; row++) { 72 | for (var col = 0; col < cols; col++) { 73 | var i = row * cols + col, 74 | idx = i * 4; 75 | data[idx] = data[idx+1] = data[idx+2] = Math.round(((g[i * 3 + 2] - min) / spread) * 255); 76 | data[idx+3] = 255; 77 | } 78 | } 79 | context.putImageData(d, 0, 0); 80 | return canvas; 81 | }; 82 | -------------------------------------------------------------------------------- /src/influences.js: -------------------------------------------------------------------------------- 1 | // Allows placing geometrically-described features on a terrain. 2 | // If you want these features to look a little less regular, apply them before a procedural pass. 3 | // If you want more complex influence, you can composite heightmaps. 4 | 5 | /** 6 | * Equations describing geographic features. 7 | */ 8 | THREE.Terrain.Influences = { 9 | Mesa: function(x) { 10 | return 1.25 * Math.min(0.8, Math.exp(-(x*x))); 11 | }, 12 | Hole: function(x) { 13 | return -THREE.Terrain.Influences.Mesa(x); 14 | }, 15 | Hill: function(x) { 16 | // Same curve as EaseInOut, but mirrored and translated. 17 | return x < 0 ? (x+1)*(x+1)*(3-2*(x+1)) : 1-x*x*(3-2*x); 18 | }, 19 | Valley: function(x) { 20 | return -THREE.Terrain.Influences.Hill(x); 21 | }, 22 | Dome: function(x) { 23 | // Parabola 24 | return -(x+1)*(x-1); 25 | }, 26 | // Not meaningful in Additive or Subtractive mode 27 | Flat: function(x) { 28 | return 0; 29 | }, 30 | Volcano: function(x) { 31 | return 0.94 - 0.32 * (Math.abs(2 * x) + Math.cos(2 * Math.PI * Math.abs(x) + 0.4)); 32 | }, 33 | }; 34 | 35 | /** 36 | * Place a geographic feature on the terrain. 37 | * 38 | * @param {THREE.Vector3[]} g 39 | * The vertex array for plane geometry to modify with heightmap data. This 40 | * method sets the `z` property of each vertex. 41 | * @param {Object} options 42 | * A map of settings that control how the terrain is constructed and 43 | * displayed. Valid values are the same as those for the `options` parameter 44 | * of {@link THREE.Terrain}(). 45 | * @param {Function} f 46 | * A function describing the feature. The function should accept one 47 | * parameter representing the distance from the feature's origin expressed as 48 | * a number between -1 and 1 inclusive. Optionally it can accept a second and 49 | * third parameter, which are the x- and y- distances from the feature's 50 | * origin, respectively. It should return a number between -1 and 1 51 | * representing the height of the feature at the given coordinate. 52 | * `THREE.Terrain.Influences` contains some useful functions for this 53 | * purpose. 54 | * @param {Number} [x=0.5] 55 | * How far across the terrain the feature should be placed on the X-axis, in 56 | * PERCENT (as a decimal) of the size of the terrain on that axis. 57 | * @param {Number} [y=0.5] 58 | * How far across the terrain the feature should be placed on the Y-axis, in 59 | * PERCENT (as a decimal) of the size of the terrain on that axis. 60 | * @param {Number} [r=64] 61 | * The radius of the feature. 62 | * @param {Number} [h=64] 63 | * The height of the feature. 64 | * @param {String} [t=THREE.NormalBlending] 65 | * Determines how to layer the feature on top of the existing terrain. Valid 66 | * values include `THREE.AdditiveBlending`, `THREE.SubtractiveBlending`, 67 | * `THREE.MultiplyBlending`, `THREE.NoBlending`, `THREE.NormalBlending`, and 68 | * any function that takes the terrain's current height, the feature's 69 | * displacement at a vertex, and the vertex's distance from the feature 70 | * origin, and returns the new height for that vertex. (If a custom function 71 | * is passed, it can take optional fourth and fifth parameters, which are the 72 | * x- and y-distances from the feature's origin, respectively.) 73 | * @param {Number/Function} [e=THREE.Terrain.EaseIn] 74 | * A function that determines the "falloff" of the feature, i.e. how quickly 75 | * the terrain will get close to its height before the feature was applied as 76 | * the distance increases from the feature's location. It does this by 77 | * interpolating the height of each vertex along a curve. Valid values 78 | * include `THREE.Terrain.Linear`, `THREE.Terrain.EaseIn`, 79 | * `THREE.Terrain.EaseOut`, `THREE.Terrain.EaseInOut`, 80 | * `THREE.Terrain.InEaseOut`, and any custom function that accepts a float 81 | * between 0 and 1 representing the distance to the feature origin and 82 | * returns a float between 0 and 1 with the adjusted distance. (Custom 83 | * functions can also accept optional second and third parameters, which are 84 | * the x- and y-distances to the feature origin, respectively.) 85 | */ 86 | THREE.Terrain.Influence = function(g, options, f, x, y, r, h, t, e) { 87 | f = f || THREE.Terrain.Influences.Hill; // feature shape 88 | x = typeof x === 'undefined' ? 0.5 : x; // x-location % 89 | y = typeof y === 'undefined' ? 0.5 : y; // y-location % 90 | r = typeof r === 'undefined' ? 64 : r; // radius 91 | h = typeof h === 'undefined' ? 64 : h; // height 92 | t = typeof t === 'undefined' ? THREE.NormalBlending : t; // blending 93 | e = e || THREE.Terrain.EaseIn; // falloff 94 | // Find the vertex location of the feature origin 95 | var xl = options.xSegments + 1, // # x-vertices 96 | yl = options.ySegments + 1, // # y-vertices 97 | vx = xl * x, // vertex x-location 98 | vy = yl * y, // vertex y-location 99 | xw = options.xSize / options.xSegments, // width of x-segments 100 | yw = options.ySize / options.ySegments, // width of y-segments 101 | rx = r / xw, // radius of the feature in vertices on the x-axis 102 | ry = r / yw, // radius of the feature in vertices on the y-axis 103 | r1 = 1 / r, // for speed 104 | xs = Math.ceil(vx - rx), // starting x-vertex index 105 | xe = Math.floor(vx + rx), // ending x-vertex index 106 | ys = Math.ceil(vy - ry), // starting y-vertex index 107 | ye = Math.floor(vy + ry); // ending y-vertex index 108 | // Walk over the vertices within radius of origin 109 | for (var i = xs; i < xe; i++) { 110 | for (var j = ys; j < ye; j++) { 111 | var k = j * xl + i, 112 | // distance to the feature origin 113 | fdx = (i - vx) * xw, 114 | fdy = (j - vy) * yw, 115 | fd = Math.sqrt(fdx*fdx + fdy*fdy), 116 | fdr = fd * r1, 117 | fdxr = fdx * r1, 118 | fdyr = fdy * r1, 119 | // Get the displacement according to f, multiply it by h, 120 | // interpolate using e, then blend according to t. 121 | d = f(fdr, fdxr, fdyr) * h * (1 - e(fdr, fdxr, fdyr)); 122 | if (fd > r || typeof g[k] == 'undefined') continue; 123 | if (t === THREE.AdditiveBlending) g[k] += d; // jscs:ignore requireSpaceAfterKeywords 124 | else if (t === THREE.SubtractiveBlending) g[k] -= d; 125 | else if (t === THREE.MultiplyBlending) g[k] *= d; 126 | else if (t === THREE.NoBlending) g[k] = d; 127 | else if (t === THREE.NormalBlending) g[k] = e(fdr, fdxr, fdyr) * g[k] + d; 128 | else if (typeof t === 'function') g[k] = t(g[k].z, d, fdr, fdxr, fdyr); 129 | } 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /src/materials.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a material that blends together textures based on vertex height. 3 | * 4 | * Inspired by http://www.chandlerprall.com/2011/06/blending-webgl-textures/ 5 | * 6 | * Usage: 7 | * 8 | * // Assuming the textures are already loaded 9 | * var material = THREE.Terrain.generateBlendedMaterial([ 10 | * {texture: THREE.ImageUtils.loadTexture('img1.jpg')}, 11 | * {texture: THREE.ImageUtils.loadTexture('img2.jpg'), levels: [-80, -35, 20, 50]}, 12 | * {texture: THREE.ImageUtils.loadTexture('img3.jpg'), levels: [20, 50, 60, 85]}, 13 | * {texture: THREE.ImageUtils.loadTexture('img4.jpg'), glsl: '1.0 - smoothstep(65.0 + smoothstep(-256.0, 256.0, vPosition.x) * 10.0, 80.0, vPosition.z)'}, 14 | * ]); 15 | * 16 | * This material tries to behave exactly like a MeshLambertMaterial other than 17 | * the fact that it blends multiple texture maps together, although 18 | * ShaderMaterials are treated slightly differently by Three.js so YMMV. Note 19 | * that this means the texture will appear black unless there are lights 20 | * shining on it. 21 | * 22 | * @param {Object[]} textures 23 | * An array of objects specifying textures to blend together and how to blend 24 | * them. Each object should have a `texture` property containing a 25 | * `THREE.Texture` instance. There must be at least one texture and the first 26 | * texture does not need any other properties because it will serve as the 27 | * base, showing up wherever another texture isn't blended in. Other textures 28 | * must have either a `levels` property containing an array of four numbers 29 | * or a `glsl` property containing a single GLSL expression evaluating to a 30 | * float between 0.0 and 1.0. For the `levels` property, the four numbers 31 | * are, in order: the height at which the texture will start blending in, the 32 | * height at which it will be fully blended in, the height at which it will 33 | * start blending out, and the height at which it will be fully blended out. 34 | * The `vec3 vPosition` variable is available to `glsl` expressions; it 35 | * contains the coordinates in Three-space of the texel currently being 36 | * rendered. 37 | * @param {Three.Material} material 38 | * An optional base material. You can use this to pick a different base 39 | * material type such as `MeshStandardMaterial` instead of the default 40 | * `MeshLambertMaterial`. 41 | */ 42 | THREE.Terrain.generateBlendedMaterial = function(textures, material) { 43 | // Convert numbers to strings of floats so GLSL doesn't barf on "1" instead of "1.0" 44 | function glslifyNumber(n) { 45 | return n === (n|0) ? n+'.0' : n+''; 46 | } 47 | 48 | var declare = '', 49 | assign = '', 50 | t0Repeat = textures[0].texture.repeat, 51 | t0Offset = textures[0].texture.offset; 52 | for (var i = 0, l = textures.length; i < l; i++) { 53 | // Update textures 54 | textures[i].texture.wrapS = textures[i].wrapT = THREE.RepeatWrapping; 55 | textures[i].texture.needsUpdate = true; 56 | 57 | // Shader fragments 58 | // Declare each texture, then mix them together. 59 | declare += 'uniform sampler2D texture_' + i + ';\n'; 60 | if (i !== 0) { 61 | var v = textures[i].levels, // Vertex heights at which to blend textures in and out 62 | p = textures[i].glsl, // Or specify a GLSL expression that evaluates to a float between 0.0 and 1.0 indicating how opaque the texture should be at this texel 63 | useLevels = typeof v !== 'undefined', // Use levels if they exist; otherwise, use the GLSL expression 64 | tiRepeat = textures[i].texture.repeat, 65 | tiOffset = textures[i].texture.offset; 66 | if (useLevels) { 67 | // Must fade in; can't start and stop at the same point. 68 | // So, if levels are too close, move one of them slightly. 69 | if (v[1] - v[0] < 1) v[0] -= 1; 70 | if (v[3] - v[2] < 1) v[3] += 1; 71 | for (var j = 0; j < v.length; j++) { 72 | v[j] = glslifyNumber(v[j]); 73 | } 74 | } 75 | // The transparency of the new texture when it is layered on top of the existing color at this texel is 76 | // (how far between the start-blending-in and fully-blended-in levels the current vertex is) + 77 | // (how far between the start-blending-out and fully-blended-out levels the current vertex is) 78 | // So the opacity is 1.0 minus that. 79 | var blendAmount = !useLevels ? p : 80 | '1.0 - smoothstep(' + v[0] + ', ' + v[1] + ', vPosition.z) + smoothstep(' + v[2] + ', ' + v[3] + ', vPosition.z)'; 81 | assign += ' color = mix( ' + 82 | 'texture2D( texture_' + i + ', MyvUv * vec2( ' + glslifyNumber(tiRepeat.x) + ', ' + glslifyNumber(tiRepeat.y) + ' ) + vec2( ' + glslifyNumber(tiOffset.x) + ', ' + glslifyNumber(tiOffset.y) + ' ) ), ' + 83 | 'color, ' + 84 | 'max(min(' + blendAmount + ', 1.0), 0.0)' + 85 | ');\n'; 86 | } 87 | } 88 | 89 | var fragBlend = 'float slope = acos(max(min(dot(myNormal, vec3(0.0, 0.0, 1.0)), 1.0), -1.0));\n' + 90 | ' diffuseColor = vec4( diffuse, opacity );\n' + 91 | ' vec4 color = texture2D( texture_0, MyvUv * vec2( ' + glslifyNumber(t0Repeat.x) + ', ' + glslifyNumber(t0Repeat.y) + ' ) + vec2( ' + glslifyNumber(t0Offset.x) + ', ' + glslifyNumber(t0Offset.y) + ' ) ); // base\n' + 92 | assign + 93 | ' diffuseColor = color;\n'; 94 | 95 | var fragPars = declare + '\n' + 96 | 'varying vec2 MyvUv;\n' + 97 | 'varying vec3 vPosition;\n' + 98 | 'varying vec3 myNormal;\n'; 99 | 100 | var mat = material || new THREE.MeshLambertMaterial(); 101 | mat.onBeforeCompile = function(shader) { 102 | // Patch vertexShader to setup MyUv, vPosition, and myNormal 103 | shader.vertexShader = shader.vertexShader.replace('#include ', 104 | 'varying vec2 MyvUv;\nvarying vec3 vPosition;\nvarying vec3 myNormal;\n#include '); 105 | shader.vertexShader = shader.vertexShader.replace('#include ', 106 | 'MyvUv = uv;\nvPosition = position;\nmyNormal = normal;\n#include '); 107 | 108 | shader.fragmentShader = shader.fragmentShader.replace('#include ', fragPars + '\n#include '); 109 | shader.fragmentShader = shader.fragmentShader.replace('#include ', fragBlend); 110 | 111 | // Add our custom texture uniforms 112 | for (var i = 0, l = textures.length; i < l; i++) { 113 | shader.uniforms['texture_' + i] = { 114 | type: 't', 115 | value: textures[i].texture, 116 | }; 117 | } 118 | }; 119 | 120 | return mat; 121 | }; 122 | -------------------------------------------------------------------------------- /src/noise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplex and Perlin noise. 3 | * 4 | * Copied with small edits from https://github.com/josephg/noisejs which is 5 | * public domain. Originally by Stefan Gustavson (stegu@itn.liu.se) with 6 | * optimizations by Peter Eastman (peastman@drizzle.stanford.edu) and converted 7 | * to JavaScript by Joseph Gentle. 8 | */ 9 | 10 | (function(global) { 11 | var module = global.noise = {}; 12 | 13 | function Grad(x, y, z) { 14 | this.x = x; 15 | this.y = y; 16 | this.z = z; 17 | } 18 | 19 | Grad.prototype.dot2 = function(x, y) { 20 | return this.x*x + this.y*y; 21 | }; 22 | 23 | Grad.prototype.dot3 = function(x, y, z) { 24 | return this.x*x + this.y*y + this.z*z; 25 | }; 26 | 27 | var grad3 = [ 28 | new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0), 29 | new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1), 30 | new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1), 31 | ]; 32 | 33 | var p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103, 34 | 30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94, 35 | 252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171, 36 | 168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122, 37 | 60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161, 38 | 1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159, 39 | 86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147, 40 | 118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183, 41 | 170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129, 42 | 22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228, 43 | 251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239, 44 | 107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4, 45 | 150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215, 46 | 61,156,180]; 47 | // To avoid the need for index wrapping, double the permutation table length 48 | var perm = new Array(512), 49 | gradP = new Array(512); 50 | 51 | // This isn't a very good seeding function, but it works okay. It supports 52 | // 2^16 different seed values. Write your own if you need more seeds. 53 | module.seed = function(seed) { 54 | if (seed > 0 && seed < 1) { 55 | // Scale the seed out 56 | seed *= 65536; 57 | } 58 | 59 | seed = Math.floor(seed); 60 | if (seed < 256) { 61 | seed |= seed << 8; 62 | } 63 | 64 | for (var i = 0; i < 256; i++) { 65 | var v; 66 | if (i & 1) { 67 | v = p[i] ^ (seed & 255); 68 | } 69 | else { 70 | v = p[i] ^ ((seed>>8) & 255); 71 | } 72 | 73 | perm[i] = perm[i + 256] = v; 74 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 75 | } 76 | }; 77 | 78 | module.seed(Math.random()); 79 | 80 | // Skewing and unskewing factors for 2 and 3 dimensions 81 | var F2 = 0.5*(Math.sqrt(3)-1), 82 | G2 = (3-Math.sqrt(3))/6, 83 | F3 = 1/3, 84 | G3 = 1/6; 85 | 86 | // 2D simplex noise 87 | module.simplex = function(xin, yin) { 88 | var n0, n1, n2; // Noise contributions from the three corners 89 | // Skew the input space to determine which simplex cell we're in 90 | var s = (xin+yin)*F2; // Hairy factor for 2D 91 | var i = Math.floor(xin+s); 92 | var j = Math.floor(yin+s); 93 | var t = (i+j)*G2; 94 | var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed 95 | var y0 = yin-j+t; 96 | // For the 2D case, the simplex shape is an equilateral triangle. 97 | // Determine which simplex we are in. 98 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 99 | if (x0 > y0) { // Lower triangle, XY order: (0,0)->(1,0)->(1,1) 100 | i1 = 1; j1 = 0; 101 | } 102 | else { // Upper triangle, YX order: (0,0)->(0,1)->(1,1) 103 | i1 = 0; j1 = 1; 104 | } 105 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 106 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 107 | // c = (3-sqrt(3))/6 108 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 109 | var y1 = y0 - j1 + G2; 110 | var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords 111 | var y2 = y0 - 1 + 2 * G2; 112 | // Work out the hashed gradient indices of the three simplex corners 113 | i &= 255; 114 | j &= 255; 115 | var gi0 = gradP[i+perm[j]]; 116 | var gi1 = gradP[i+i1+perm[j+j1]]; 117 | var gi2 = gradP[i+1+perm[j+1]]; 118 | // Calculate the contribution from the three corners 119 | var t0 = 0.5 - x0*x0-y0*y0; 120 | if (t0 < 0) { 121 | n0 = 0; 122 | } 123 | else { 124 | t0 *= t0; 125 | n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient 126 | } 127 | var t1 = 0.5 - x1*x1-y1*y1; 128 | if (t1 < 0) { 129 | n1 = 0; 130 | } 131 | else { 132 | t1 *= t1; 133 | n1 = t1 * t1 * gi1.dot2(x1, y1); 134 | } 135 | var t2 = 0.5 - x2*x2-y2*y2; 136 | if (t2 < 0) { 137 | n2 = 0; 138 | } 139 | else { 140 | t2 *= t2; 141 | n2 = t2 * t2 * gi2.dot2(x2, y2); 142 | } 143 | // Add contributions from each corner to get the final noise value. 144 | // The result is scaled to return values in the interval [-1,1]. 145 | return 70 * (n0 + n1 + n2); 146 | }; 147 | 148 | // ##### Perlin noise stuff 149 | 150 | function fade(t) { 151 | return t*t*t*(t*(t*6-15)+10); 152 | } 153 | 154 | function lerp(a, b, t) { 155 | return (1-t)*a + t*b; 156 | } 157 | 158 | // 2D Perlin Noise 159 | module.perlin = function(x, y) { 160 | // Find unit grid cell containing point 161 | var X = Math.floor(x), 162 | Y = Math.floor(y); 163 | // Get relative xy coordinates of point within that cell 164 | x = x - X; 165 | y = y - Y; 166 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 167 | X = X & 255; 168 | Y = Y & 255; 169 | 170 | // Calculate noise contributions from each of the four corners 171 | var n00 = gradP[X+perm[Y]].dot2(x, y); 172 | var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); 173 | var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); 174 | var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1); 175 | 176 | // Compute the fade curve value for x 177 | var u = fade(x); 178 | 179 | // Interpolate the four results 180 | return lerp( 181 | lerp(n00, n10, u), 182 | lerp(n01, n11, u), 183 | fade(y) 184 | ); 185 | }; 186 | })(this); 187 | -------------------------------------------------------------------------------- /src/scatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Scatter a mesh across the terrain. 3 | * 4 | * @param {THREE.BufferGeometry} geometry 5 | * The terrain's geometry (or the highest-resolution version of it). 6 | * @param {Object} options 7 | * A map of settings that controls how the meshes are scattered, with the 8 | * following properties: 9 | * - `mesh`: A `THREE.Mesh` instance to scatter across the terrain. 10 | * - `spread`: A number or a function that affects where meshes are placed. 11 | * If it is a number, it represents the percent of faces of the terrain 12 | * onto which a mesh should be placed. If it is a function, it takes a 13 | * vertex from the terrain and the key of a related face and returns a 14 | * boolean indicating whether to place a mesh on that face or not. An 15 | * example could be `function(v, k) { return v.z > 0 && !(k % 4); }`. 16 | * Defaults to 0.025. 17 | * - `smoothSpread`: If the `spread` option is a number, this affects how 18 | * much placement is "eased in." Specifically, if the `randomness` function 19 | * returns a value for a face that is within `smoothSpread` percentiles 20 | * above `spread`, then the probability that a mesh is placed there is 21 | * interpolated between zero and `spread`. This creates a "thinning" effect 22 | * near the edges of clumps, if the randomness function creates clumps. 23 | * - `scene`: A `THREE.Object3D` instance to which the scattered meshes will 24 | * be added. This is expected to be either a return value of a call to 25 | * `THREE.Terrain()` or added to that return value; otherwise the position 26 | * and rotation of the meshes will be wrong. 27 | * - `sizeVariance`: The percent by which instances of the mesh can be scaled 28 | * up or down when placed on the terrain. 29 | * - `randomness`: If `options.spread` is a number, then this property is a 30 | * function that determines where meshes are placed. Specifically, it 31 | * returns an array of numbers, where each number is the probability that 32 | * a mesh is NOT placed on the corresponding face. Valid values include 33 | * `Math.random` and the return value of a call to 34 | * `THREE.Terrain.ScatterHelper`. 35 | * - `maxSlope`: The angle in radians between the normal of a face of the 36 | * terrain and the "up" vector above which no mesh will be placed on the 37 | * related face. Defaults to ~0.63, which is 36 degrees. 38 | * - `maxTilt`: The maximum angle in radians a mesh can be tilted away from 39 | * the "up" vector (towards the normal vector of the face of the terrain). 40 | * Defaults to Infinity (meshes will point towards the normal). 41 | * - `w`: The number of horizontal segments of the terrain. 42 | * - `h`: The number of vertical segments of the terrain. 43 | * 44 | * @return {THREE.Object3D} 45 | * An Object3D containing the scattered meshes. This is the value of the 46 | * `options.scene` parameter if passed. This is expected to be either a 47 | * return value of a call to `THREE.Terrain()` or added to that return value; 48 | * otherwise the position and rotation of the meshes will be wrong. 49 | */ 50 | THREE.Terrain.ScatterMeshes = function(geometry, options) { 51 | if (!options.mesh) { 52 | console.error('options.mesh is required for THREE.Terrain.ScatterMeshes but was not passed'); 53 | return; 54 | } 55 | if (!options.scene) { 56 | options.scene = new THREE.Object3D(); 57 | } 58 | var defaultOptions = { 59 | spread: 0.025, 60 | smoothSpread: 0, 61 | sizeVariance: 0.1, 62 | randomness: Math.random, 63 | maxSlope: 0.6283185307179586, // 36deg or 36 / 180 * Math.PI, about the angle of repose of earth 64 | maxTilt: Infinity, 65 | w: 0, 66 | h: 0, 67 | }; 68 | for (var opt in defaultOptions) { 69 | if (defaultOptions.hasOwnProperty(opt)) { 70 | options[opt] = typeof options[opt] === 'undefined' ? defaultOptions[opt] : options[opt]; 71 | } 72 | } 73 | 74 | var spreadIsNumber = typeof options.spread === 'number', 75 | randomHeightmap, 76 | randomness, 77 | spreadRange = 1 / options.smoothSpread, 78 | doubleSizeVariance = options.sizeVariance * 2, 79 | vertex1 = new THREE.Vector3(), 80 | vertex2 = new THREE.Vector3(), 81 | vertex3 = new THREE.Vector3(), 82 | faceNormal = new THREE.Vector3(), 83 | up = options.mesh.up.clone().applyAxisAngle(new THREE.Vector3(1, 0, 0), 0.5*Math.PI); 84 | if (spreadIsNumber) { 85 | randomHeightmap = options.randomness(); 86 | randomness = typeof randomHeightmap === 'number' ? Math.random : function(k) { return randomHeightmap[k]; }; 87 | } 88 | 89 | geometry = geometry.toNonIndexed(); 90 | var gArray = geometry.attributes.position.array; 91 | for (var i = 0; i < geometry.attributes.position.array.length; i += 9) { 92 | vertex1.set(gArray[i + 0], gArray[i + 1], gArray[i + 2]); 93 | vertex2.set(gArray[i + 3], gArray[i + 4], gArray[i + 5]); 94 | vertex3.set(gArray[i + 6], gArray[i + 7], gArray[i + 8]); 95 | THREE.Triangle.getNormal(vertex1, vertex2, vertex3, faceNormal); 96 | 97 | var place = false; 98 | if (spreadIsNumber) { 99 | var rv = randomness(i/9); 100 | if (rv < options.spread) { 101 | place = true; 102 | } 103 | else if (rv < options.spread + options.smoothSpread) { 104 | // Interpolate rv between spread and spread + smoothSpread, 105 | // then multiply that "easing" value by the probability 106 | // that a mesh would get placed on a given face. 107 | place = THREE.Terrain.EaseInOut((rv - options.spread) * spreadRange) * options.spread > Math.random(); 108 | } 109 | } 110 | else { 111 | place = options.spread(vertex1, i / 9, faceNormal, i); 112 | } 113 | if (place) { 114 | // Don't place a mesh if the angle is too steep. 115 | if (faceNormal.angleTo(up) > options.maxSlope) { 116 | continue; 117 | } 118 | var mesh = options.mesh.clone(); 119 | mesh.position.addVectors(vertex1, vertex2).add(vertex3).divideScalar(3); 120 | if (options.maxTilt > 0) { 121 | var normal = mesh.position.clone().add(faceNormal); 122 | mesh.lookAt(normal); 123 | var tiltAngle = faceNormal.angleTo(up); 124 | if (tiltAngle > options.maxTilt) { 125 | var ratio = options.maxTilt / tiltAngle; 126 | mesh.rotation.x *= ratio; 127 | mesh.rotation.y *= ratio; 128 | mesh.rotation.z *= ratio; 129 | } 130 | } 131 | mesh.rotation.x += 90 / 180 * Math.PI; 132 | mesh.rotateY(Math.random() * 2 * Math.PI); 133 | if (options.sizeVariance) { 134 | var variance = Math.random() * doubleSizeVariance - options.sizeVariance; 135 | mesh.scale.x = mesh.scale.z = 1 + variance; 136 | mesh.scale.y += variance; 137 | } 138 | 139 | mesh.updateMatrix(); 140 | options.scene.add(mesh); 141 | } 142 | } 143 | 144 | return options.scene; 145 | }; 146 | 147 | /** 148 | * Generate a function that returns a heightmap to pass to ScatterMeshes. 149 | * 150 | * Specifically, this function generates a heightmap and then uses that 151 | * heightmap as a map of probabilities of where meshes will be placed. 152 | * 153 | * @param {Function} method 154 | * A random terrain generation function (i.e. a valid value for the 155 | * `options.heightmap` parameter of the `THREE.Terrain` function). 156 | * @param {Object} options 157 | * A map of settings that control how the resulting noise should be generated 158 | * (with the same parameters as the `options` parameter to the 159 | * `THREE.Terrain` function). `options.minHeight` must equal `0` and 160 | * `options.maxHeight` must equal `1` if they are specified. 161 | * @param {Number} skip 162 | * The number of sequential faces to skip between faces that are candidates 163 | * for placing a mesh. This avoid clumping meshes too closely together. 164 | * Defaults to 1. 165 | * @param {Number} threshold 166 | * The probability that, if a mesh can be placed on a non-skipped face due to 167 | * the shape of the heightmap, a mesh actually will be placed there. Helps 168 | * thin out placement and make it less regular. Defaults to 0.25. 169 | * 170 | * @return {Function} 171 | * Returns a function that can be passed as the value of the 172 | * `options.randomness` parameter to the {@link THREE.Terrain.ScatterMeshes} 173 | * function. 174 | */ 175 | THREE.Terrain.ScatterHelper = function(method, options, skip, threshold) { 176 | skip = skip || 1; 177 | threshold = threshold || 0.25; 178 | options.frequency = options.frequency || 2.5; 179 | 180 | var clonedOptions = {}; 181 | for (var opt in options) { 182 | if (options.hasOwnProperty(opt)) { 183 | clonedOptions[opt] = options[opt]; 184 | } 185 | } 186 | 187 | clonedOptions.xSegments *= 2; 188 | clonedOptions.stretch = true; 189 | clonedOptions.maxHeight = 1; 190 | clonedOptions.minHeight = 0; 191 | var heightmap = THREE.Terrain.heightmapArray(method, clonedOptions); 192 | 193 | for (var i = 0, l = heightmap.length; i < l; i++) { 194 | if (i % skip || Math.random() > threshold) { 195 | heightmap[i] = 1; // 0 = place, 1 = don't place 196 | } 197 | } 198 | return function() { 199 | return heightmap; 200 | }; 201 | }; 202 | -------------------------------------------------------------------------------- /src/weightedBoxBlurGaussian.js: -------------------------------------------------------------------------------- 1 | // jscs:disable disallowSpaceBeforeSemicolon, requireBlocksOnNewline 2 | (function() { 3 | 4 | /** 5 | * Perform Gaussian smoothing on terrain vertices. 6 | * 7 | * @param {Float32Array} g 8 | * The geometry's z-positions to modify with heightmap data. 9 | * @param {Object} options 10 | * A map of settings that control how the terrain is constructed and 11 | * displayed. Valid values are the same as those for the `options` parameter 12 | * of {@link THREE.Terrain}(). 13 | * @param {Number} [s=1] 14 | * The standard deviation of the Gaussian kernel to use. Higher values result 15 | * in smoothing across more cells of the src matrix. 16 | * @param {Number} [n=3] 17 | * The number of box blurs to use in the approximation. Larger values result 18 | * in slower but more accurate smoothing. 19 | */ 20 | THREE.Terrain.GaussianBoxBlur = function(g, options, s, n) { 21 | gaussianBoxBlur( 22 | g, 23 | options.xSegments+1, 24 | options.ySegments+1, 25 | s, 26 | n 27 | ); 28 | }; 29 | 30 | /** 31 | * Approximate a Gaussian blur by performing several weighted box blurs. 32 | * 33 | * After this function runs, `tcl` will contain the blurred source channel. 34 | * This operation also modifies `scl`. 35 | * 36 | * Lightly modified from http://blog.ivank.net/fastest-gaussian-blur.html 37 | * under the MIT license: http://opensource.org/licenses/MIT 38 | * 39 | * Other than style cleanup, the main significant change is that the original 40 | * version was used for manipulating RGBA channels in an image, so it assumed 41 | * that input and output were integers [0, 255]. This version does not make 42 | * such assumptions about the input or output values. 43 | * 44 | * @param Number[] scl 45 | * The source channel. 46 | * @param Number w 47 | * The image width. 48 | * @param Number h 49 | * The image height. 50 | * @param Number [r=1] 51 | * The standard deviation (how much to blur). 52 | * @param Number [n=3] 53 | * The number of box blurs to use in the approximation. 54 | * @param Number[] [tcl] 55 | * The target channel. Should be different than the source channel. If not 56 | * passed, one is created. This is also the return value. 57 | * 58 | * @return Number[] 59 | * An array representing the blurred channel. 60 | */ 61 | function gaussianBoxBlur(scl, w, h, r, n, tcl) { 62 | if (typeof r === 'undefined') r = 1; 63 | if (typeof n === 'undefined') n = 3; 64 | if (typeof tcl === 'undefined') tcl = new Float32Array(scl.length); 65 | var boxes = boxesForGauss(r, n); 66 | for (var i = 0; i < n; i++) { 67 | boxBlur(scl, tcl, w, h, (boxes[i]-1)/2); 68 | } 69 | return tcl; 70 | } 71 | 72 | /** 73 | * Calculate the size of boxes needed to approximate a Gaussian blur. 74 | * 75 | * The appropriate box sizes depend on the number of box blur passes required. 76 | * 77 | * @param Number sigma 78 | * The standard deviation (how much to blur). 79 | * @param Number n 80 | * The number of boxes (also the number of box blurs you want to perform 81 | * using those boxes). 82 | */ 83 | function boxesForGauss(sigma, n) { 84 | // Calculate how far out we need to go to capture the bulk of the distribution. 85 | var wIdeal = Math.sqrt(12*sigma*sigma/n + 1); // Ideal averaging filter width 86 | var wl = Math.floor(wIdeal); // Lower odd integer bound on the width 87 | if (wl % 2 === 0) wl--; 88 | var wu = wl+2; // Upper odd integer bound on the width 89 | 90 | var mIdeal = (12*sigma*sigma - n*wl*wl - 4*n*wl - 3*n)/(-4*wl - 4); 91 | var m = Math.round(mIdeal); 92 | // var sigmaActual = Math.sqrt( (m*wl*wl + (n-m)*wu*wu - n)/12 ); 93 | 94 | var sizes = new Int16Array(n); 95 | for (var i = 0; i < n; i++) { sizes[i] = i < m ? wl : wu; } 96 | return sizes; 97 | } 98 | 99 | /** 100 | * Perform a 2D box blur by doing a 1D box blur in two directions. 101 | * 102 | * Uses the same parameters as gaussblur(). 103 | */ 104 | function boxBlur(scl, tcl, w, h, r) { 105 | for (var i = 0, l = scl.length; i < l; i++) { tcl[i] = scl[i]; } 106 | boxBlurH(tcl, scl, w, h, r); 107 | boxBlurV(scl, tcl, w, h, r); 108 | } 109 | 110 | /** 111 | * Perform a horizontal box blur. 112 | * 113 | * Uses the same parameters as gaussblur(). 114 | */ 115 | function boxBlurH(scl, tcl, w, h, r) { 116 | var iarr = 1 / (r+r+1); // averaging adjustment parameter 117 | for (var i = 0; i < h; i++) { 118 | var ti = i * w, // current target index 119 | li = ti, // current left side of the examined range 120 | ri = ti + r, // current right side of the examined range 121 | fv = scl[ti], // first value in the row 122 | lv = scl[ti + w - 1], // last value in the row 123 | val = (r+1) * fv, // target value, accumulated over examined points 124 | j; 125 | // Sum the source values in the box 126 | for (j = 0; j < r; j++) { val += scl[ti + j]; } 127 | // Compute the target value by taking the average of the surrounding 128 | // values. This is done by adding the deviations so far and adjusting, 129 | // accounting for the edges by extending the first and last values. 130 | for (j = 0 ; j <= r ; j++) { val += scl[ri++] - fv ; tcl[ti++] = val*iarr; } 131 | for (j = r+1; j < w-r; j++) { val += scl[ri++] - scl[li++]; tcl[ti++] = val*iarr; } 132 | for (j = w-r; j < w ; j++) { val += lv - scl[li++]; tcl[ti++] = val*iarr; } 133 | } 134 | } 135 | 136 | /** 137 | * Perform a vertical box blur. 138 | * 139 | * Uses the same parameters as gaussblur(). 140 | */ 141 | function boxBlurV(scl, tcl, w, h, r) { 142 | var iarr = 1 / (r+r+1); // averaging adjustment parameter 143 | for (var i = 0; i < w; i++) { 144 | var ti = i, // current target index 145 | li = ti, // current top of the examined range 146 | ri = ti+r*w, // current bottom of the examined range 147 | fv = scl[ti], // first value in the column 148 | lv = scl[ti + w*(h-1)], // last value in the column 149 | val = (r+1) * fv, // target value, accumulated over examined points 150 | j; 151 | // Sum the source values in the box 152 | for (j = 0; j < r; j++) { val += scl[ti + j * w]; } 153 | // Compute the target value by taking the average of the surrounding 154 | // values. This is done by adding the deviations so far and adjusting, 155 | // accounting for the edges by extending the first and last values. 156 | for (j = 0 ; j <= r ; j++) { val += scl[ri] - fv ; tcl[ti] = val*iarr; ri+=w; ti+=w; } 157 | for (j = r+1; j < h-r; j++) { val += scl[ri] - scl[li]; tcl[ti] = val*iarr; li+=w; ri+=w; ti+=w; } 158 | for (j = h-r; j < h ; j++) { val += lv - scl[li]; tcl[ti] = val*iarr; li+=w; ti+=w; } 159 | } 160 | } 161 | 162 | })(); 163 | -------------------------------------------------------------------------------- /src/worley.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /* 3 | * A set of functions to calculate the 2D distance between two vectors. 4 | * 5 | * The other alternatives are distanceTo (Euclidean) and distanceToSquared 6 | * (Euclidean squared). 7 | */ 8 | THREE.Vector2.prototype.distanceToManhattan = function(b) { 9 | return Math.abs(this.x - b.x) + Math.abs(this.y - b.y); 10 | }; 11 | THREE.Vector2.prototype.distanceToChebyshev = function(b) { 12 | var c = Math.abs(this.x - b.x), 13 | d = Math.abs(this.y - b.y); 14 | return c <= d ? d : c; 15 | }; 16 | THREE.Vector2.prototype.distanceToQuadratic = function(b) { 17 | var c = Math.abs(this.x - b.x), 18 | d = Math.abs(this.y - b.y); 19 | return c*c + c*d + d*d; 20 | }; 21 | 22 | /** 23 | * Find the Voronoi centroid closest to the current terrain vertex. 24 | * 25 | * This approach is naive, but since the number of cells isn't typically 26 | * very big, it's plenty fast enough. 27 | * 28 | * Alternatives approaches include using Fortune's algorithm or tracking 29 | * cells based on a grid. 30 | */ 31 | function distanceToNearest(coords, points, distanceType) { 32 | var color = Infinity, 33 | distanceFunc = 'distanceTo' + distanceType; 34 | for (var k = 0; k < points.length; k++) { 35 | var d = points[k][distanceFunc](coords); 36 | if (d < color) { 37 | color = d; 38 | } 39 | } 40 | return color; 41 | } 42 | 43 | /** 44 | * Generate random terrain using Worley noise. 45 | * 46 | * Worley noise is also known as Cell or Voronoi noise. It is generated by 47 | * scattering a bunch of points in heightmap-space, then setting the height 48 | * of every point in the heightmap based on how close it is to the closest 49 | * scattered point (or the nth-closest point, but this results in 50 | * heightmaps that don't look much like terrain). 51 | * 52 | * @param {Float32Array} g 53 | * The geometry's z-positions to modify with heightmap data. 54 | * @param {Object} options 55 | * A map of settings that control how the terrain is constructed and 56 | * displayed. Valid values are the same as those for the `options` 57 | * parameter of {@link THREE.Terrain}(), plus three additional available 58 | * properties: 59 | * - `distanceType`: The name of a method to use to calculate the 60 | * distance between a point in the heightmap and a Voronoi centroid in 61 | * order to determine the height of that point. Available methods 62 | * include 'Manhattan', 'Chebyshev', 'Quadratic', 'Squared' (squared 63 | * Euclidean), and '' (the empty string, meaning Euclidean, the 64 | * default). 65 | * - `worleyDistanceTransformation`: A function that takes the distance 66 | * from a heightmap vertex to a Voronoi centroid and returns a relative 67 | * height for that vertex. Defaults to function(d) { return -d; }. 68 | * Interesting choices of algorithm include 69 | * `0.5 + 1.0 * Math.cos((0.5*d-1) * Math.PI) - d`, which produces 70 | * interesting stepped cones, and `-Math.sqrt(d)`, which produces sharp 71 | * peaks resembling stalagmites. 72 | * - `worleyDistribution`: A function to use to distribute Voronoi 73 | * centroids. Available methods include 74 | * `THREE.Terrain.Worley.randomPoints` (the default), 75 | * `THREE.Terrain.Worley.PoissonDisks`, and any function that returns 76 | * an array of `THREE.Vector2` instances. You can wrap the PoissonDisks 77 | * function to use custom parameters. 78 | * - `worleyPoints`: The number of Voronoi cells to use (must be at least 79 | * one). Calculated by default based on the size of the terrain. 80 | */ 81 | THREE.Terrain.Worley = function(g, options) { 82 | var points = (options.worleyDistribution || THREE.Terrain.Worley.randomPoints)(options.xSegments, options.ySegments, options.worleyPoints), 83 | transform = options.worleyDistanceTransformation || function(d) { return -d; }, 84 | currentCoords = new THREE.Vector2(0, 0); 85 | // The height of each heightmap vertex is the distance to the closest Voronoi centroid 86 | for (var i = 0, xl = options.xSegments + 1; i < xl; i++) { 87 | for (var j = 0; j < options.ySegments + 1; j++) { 88 | currentCoords.x = i; 89 | currentCoords.y = j; 90 | g[j*xl+i] = transform(distanceToNearest(currentCoords, points, options.distanceType || '')); 91 | } 92 | } 93 | // We set the heights to distances so now we need to normalize 94 | THREE.Terrain.Clamp(g, { 95 | maxHeight: options.maxHeight, 96 | minHeight: options.minHeight, 97 | stretch: true, 98 | }); 99 | }; 100 | 101 | /** 102 | * Randomly distribute points in space. 103 | */ 104 | THREE.Terrain.Worley.randomPoints = function(width, height, numPoints) { 105 | numPoints = numPoints || Math.floor(Math.sqrt(width * height * 0.025)) || 1; 106 | var points = new Array(numPoints); 107 | for (var i = 0; i < numPoints; i++) { 108 | points[i] = new THREE.Vector2( 109 | Math.random() * width, 110 | Math.random() * height 111 | ); 112 | } 113 | return points; 114 | }; 115 | 116 | /* Utility functions for Poisson Disks. */ 117 | 118 | function removeAndReturnRandomElement(arr) { 119 | return arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; 120 | } 121 | 122 | function putInGrid(grid, point, cellSize) { 123 | var gx = Math.floor(point.x / cellSize), 124 | gy = Math.floor(point.y / cellSize); 125 | if (!grid[gx]) grid[gx] = []; 126 | grid[gx][gy] = point; 127 | } 128 | 129 | function inRectangle(point, width, height) { 130 | return point.x >= 0 && // jscs:ignore requireSpaceAfterKeywords 131 | point.y >= 0 && 132 | point.x <= width+1 && 133 | point.y <= height+1; 134 | } 135 | 136 | function inNeighborhood(grid, point, minDist, cellSize) { 137 | var gx = Math.floor(point.x / cellSize), 138 | gy = Math.floor(point.y / cellSize); 139 | for (var x = gx - 1; x <= gx + 1; x++) { 140 | for (var y = gy - 1; y <= gy + 1; y++) { 141 | if (x !== gx && y !== gy && 142 | typeof grid[x] !== 'undefined' && typeof grid[x][y] !== 'undefined') { 143 | var cx = x * cellSize, 144 | cy = y * cellSize; 145 | if (Math.sqrt((point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy)) < minDist) { 146 | return true; 147 | } 148 | } 149 | } 150 | } 151 | return false; 152 | } 153 | 154 | function generateRandomPointAround(point, minDist) { 155 | var radius = minDist * (Math.random() + 1), 156 | angle = 2 * Math.PI * Math.random(); 157 | return new THREE.Vector2( 158 | point.x + radius * Math.cos(angle), 159 | point.y + radius * Math.sin(angle) 160 | ); 161 | } 162 | 163 | /** 164 | * Generate a set of points using Poisson disk sampling. 165 | * 166 | * Useful for clustering scattered meshes and Voronoi cells for Worley noise. 167 | * 168 | * Ported from pseudocode at http://devmag.org.za/2009/05/03/poisson-disk-sampling/ 169 | * 170 | * @param {Object} options 171 | * A map of settings that control how the resulting noise should be generated 172 | * (with the same parameters as the `options` parameter to the 173 | * `THREE.Terrain` function). 174 | * 175 | * @return {THREE.Vector2[]} 176 | * An array of points. 177 | */ 178 | THREE.Terrain.Worley.PoissonDisks = function(width, height, numPoints, minDist) { 179 | numPoints = numPoints || Math.floor(Math.sqrt(width * height * 0.2)) || 1; 180 | minDist = Math.sqrt((width + height) * 2.5); 181 | if (minDist > numPoints * 0.67) minDist = numPoints * 0.67; 182 | var cellSize = minDist / Math.sqrt(2); 183 | if (cellSize < 2) cellSize = 2; 184 | 185 | var grid = []; 186 | 187 | var processList = [], 188 | samplePoints = []; 189 | 190 | var firstPoint = new THREE.Vector2( 191 | Math.random() * width, 192 | Math.random() * height 193 | ); 194 | processList.push(firstPoint); 195 | samplePoints.push(firstPoint); 196 | putInGrid(grid, firstPoint, cellSize); 197 | 198 | var count = 0; 199 | while (processList.length) { 200 | var point = removeAndReturnRandomElement(processList); 201 | for (var i = 0; i < numPoints; i++) { 202 | // optionally, minDist = perlin(point.x / width, point.y / height) 203 | var newPoint = generateRandomPointAround(point, minDist); 204 | if (inRectangle(newPoint, width, height) && !inNeighborhood(grid, newPoint, minDist, cellSize)) { 205 | processList.push(newPoint); 206 | samplePoints.push(newPoint); 207 | putInGrid(grid, newPoint, cellSize); 208 | if (samplePoints.length >= numPoints) break; 209 | } 210 | } 211 | if (samplePoints.length >= numPoints) break; 212 | // Sanity check 213 | if (++count > numPoints*numPoints) { 214 | break; 215 | } 216 | } 217 | return samplePoints; 218 | }; 219 | })(); 220 | -------------------------------------------------------------------------------- /statistics/README.md: -------------------------------------------------------------------------------- 1 | This package provides statistics about each major procedural terrain generation 2 | method included in the `THREE.Terrain` library. 3 | 4 | By default, for each procedural terrain generation method, it generates 40 5 | terrains. It then provides interesting summary statistics overall and for each 6 | method. 7 | 8 | **Warning:** The calculations can take awhile to run (typically under 30 9 | seconds) and will lock up your browser while running. 10 | 11 | [Run the analysis](https://icecreamyou.github.io/THREE.Terrain/statistics/) 12 | 13 | Note that the graphs show data in the generated range, not in the maximum 14 | possible range. This makes it easier to see the shape of the histograms, even 15 | though it makes visually comparing their locations more difficult. 16 | -------------------------------------------------------------------------------- /statistics/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCreamYou/THREE.Terrain/7443ac8ac1697f8a4e315ca5fdd494984ddb200a/statistics/images/favicon.png -------------------------------------------------------------------------------- /statistics/images/heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IceCreamYou/THREE.Terrain/7443ac8ac1697f8a4e315ca5fdd494984ddb200a/statistics/images/heightmap.png -------------------------------------------------------------------------------- /statistics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simulation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /statistics/scripts/index.js: -------------------------------------------------------------------------------- 1 | var heightmaps = [ 2 | 'Cosine', 3 | 'CosineLayers', 4 | 'DiamondSquare', 5 | 'Fault', 6 | 'heightmap.png', 7 | 'Hill', 8 | 'HillIsland', 9 | 'Particles', 10 | 'Perlin', 11 | 'PerlinDiamond', 12 | 'PerlinLayers', 13 | 'Simplex', 14 | 'SimplexLayers', 15 | 'Value', 16 | 'Weierstrass', 17 | 'Worley', 18 | ], 19 | easing = [ 20 | 'Linear', 21 | 'EaseIn', 22 | 'EaseOut', 23 | 'EaseInOut', 24 | 'InEaseOut', 25 | ], 26 | aProps = [ 27 | 'elevation.median', 28 | 'elevation.mean', 29 | 'elevation.iqr', 30 | 'elevation.stdev', 31 | 'elevation.pearsonSkew', 32 | 'elevation.groeneveldMeedenSkew', 33 | 'elevation.kurtosis', 34 | 'slope.median', 35 | 'slope.mean', 36 | 'slope.iqr', 37 | 'slope.stdev', 38 | 'slope.pearsonSkew', 39 | 'slope.groeneveldMeedenSkew', 40 | 'slope.kurtosis', 41 | 'roughness.planimetricAreaRatio', 42 | 'roughness.terrainRuggednessIndex', 43 | 'roughness.jaggedness', 44 | 'fittedPlane.slope', 45 | ], 46 | mat = new THREE.MeshBasicMaterial({color: 0x5566aa, wireframe: true}), 47 | heightmapImage = new Image(), 48 | n = 40, 49 | bucketCount = 10, 50 | i, 51 | j, 52 | l; 53 | heightmapImage.addEventListener('load', setup, false); 54 | 55 | function setup() { 56 | var results = {overall: {}, summary: {}}, 57 | result, 58 | analytics, 59 | options, 60 | heightmap, 61 | m, 62 | k, 63 | prop, 64 | needsDegreeSymbol, 65 | output = document.getElementById('analytics'), 66 | accumulator = function(sum, value) { return sum + value; }, 67 | sum, 68 | deviation, 69 | statgroup, 70 | divMean, 71 | divStdev, 72 | histogramContainer, 73 | histogramLabel, 74 | canvas; 75 | 76 | // Gather data 77 | for (i = 0, l = aProps.length; i < l; i++) { 78 | results.overall[aProps[i]] = []; 79 | } 80 | for (i = 0, l = heightmaps.length; i < l; i++) { 81 | heightmap = heightmaps[i]; 82 | results[heightmap] = {}; 83 | result = results[heightmap]; 84 | for (j = 0, m = aProps.length; j < m; j++) { 85 | result[aProps[j]] = []; 86 | } 87 | options = assembleOptions(heightmap); 88 | for (j = 0; j < n; j++) { 89 | analytics = THREE.Terrain.Analyze(THREE.Terrain(options).children[0], options); 90 | for (k = 0, m = aProps.length; k < m; k++) { 91 | prop = aProps[k].split('.'); 92 | result[aProps[k]].push(analytics[prop[0]][prop[1]]); 93 | results.overall[aProps[k]].push(analytics[prop[0]][prop[1]]); 94 | } 95 | } 96 | } 97 | 98 | // Summarize 99 | var outline = document.createElement('ul'); 100 | outline.id = 'outline'; 101 | outline.innerHTML += '
  • Overall
  • '; 102 | for (i = 0, l = heightmaps.length; i < l; i++) { 103 | outline.innerHTML += '
  • ' + heightmaps[i] + '
  • '; 104 | } 105 | output.appendChild(outline); 106 | var header = document.createElement('h2'), 107 | section = document.createElement('div'); 108 | header.textContent = 'Overall'; 109 | section.classList.add('section'); 110 | section.id = 'overall'; 111 | section.appendChild(header); 112 | for (i = 0, l = aProps.length; i < l; i++) { 113 | prop = aProps[i]; 114 | needsDegreeSymbol = prop.indexOf('slope') !== -1 && prop.indexOf('kurtosis') === -1 && prop.indexOf('Skew') === -1; 115 | results.summary[prop] = { 116 | mean: results.overall[prop].reduce(accumulator) / results.overall[prop].length, 117 | }; 118 | for (j = 0, m = results.overall[prop].length, sum = 0; j < m; j++) { 119 | deviation = results.overall[prop][j] - results.summary[prop].mean; 120 | sum += deviation * deviation; 121 | } 122 | results.summary[prop].stdev = Math.sqrt(sum / results.overall[prop].length); 123 | statgroup = document.createElement('div'); 124 | statgroup.classList.add('statgroup'); 125 | divMean = document.createElement('div'); 126 | divMean.classList.add('stat'); 127 | divMean.innerHTML = 'Mean of all ' + prop + 128 | '' + results.summary[prop].mean.round(3) + 129 | (needsDegreeSymbol ? '°' : '') + ''; 130 | statgroup.appendChild(divMean); 131 | divStdev = document.createElement('div'); 132 | divStdev.classList.add('stat'); 133 | divStdev.innerHTML = 'Stdev of all ' + prop + 134 | '' + results.summary[prop].stdev.round(3) + 135 | (needsDegreeSymbol ? '°' : '') + ''; 136 | statgroup.appendChild(divStdev); 137 | histogramContainer = document.createElement('div'); 138 | histogramLabel = document.createElement('div'); 139 | canvas = document.createElement('canvas'); 140 | drawHistogram( 141 | bucketNumbersLinearly( 142 | results.overall[prop], 143 | bucketCount 144 | ), 145 | canvas, 146 | undefined, 147 | undefined, 148 | needsDegreeSymbol ? String.fromCharCode(176) : undefined 149 | ); 150 | histogramContainer.classList.add('histogram-container'); 151 | histogramLabel.classList.add('graph-label'); 152 | histogramLabel.textContent = prop + ' histogram'; 153 | histogramContainer.appendChild(canvas); 154 | histogramContainer.appendChild(histogramLabel); 155 | statgroup.appendChild(histogramContainer); 156 | section.appendChild(statgroup); 157 | } 158 | output.appendChild(section); 159 | for (i = 0, l = heightmaps.length; i < l; i++) { 160 | heightmap = heightmaps[i]; 161 | result = results[heightmap]; 162 | result.summary = {}; 163 | section = document.createElement('div'); 164 | section.classList.add('section'); 165 | section.id = heightmap; 166 | header = document.createElement('h2'); 167 | header.textContent = heightmap; 168 | section.appendChild(header); 169 | for (j = 0, m = aProps.length; j < m; j++) { 170 | prop = aProps[j]; 171 | needsDegreeSymbol = prop.indexOf('slope') !== -1 && prop.indexOf('kurtosis') === -1 && prop.indexOf('Skew') === -1; 172 | result.summary[prop] = { 173 | mean: result[prop].reduce(accumulator) / result[prop].length, 174 | }; 175 | for (k = 0, sum = 0; k < n; k++) { 176 | deviation = result[prop][k] - result.summary[prop].mean; 177 | sum += deviation * deviation; 178 | } 179 | result.summary[prop].stdev = Math.sqrt(sum / result[prop].length); 180 | statgroup = document.createElement('div'); 181 | statgroup.classList.add('statgroup'); 182 | divMean = document.createElement('div'); 183 | divMean.classList.add('stat'); 184 | divMean.innerHTML = 'Mean of ' + prop + 185 | '' + result.summary[prop].mean.round(3) + 186 | (needsDegreeSymbol ? '°' : '') + ''; 187 | statgroup.appendChild(divMean); 188 | divStdev = document.createElement('div'); 189 | divStdev.classList.add('stat'); 190 | divStdev.innerHTML = 'Stdev of ' + prop + 191 | '' + result.summary[prop].stdev.round(3) + 192 | (needsDegreeSymbol ? '°' : '') + ''; 193 | statgroup.appendChild(divStdev); 194 | histogramContainer = document.createElement('div'); 195 | histogramLabel = document.createElement('div'); 196 | canvas = document.createElement('canvas'); 197 | drawHistogram( 198 | bucketNumbersLinearly( 199 | result[prop], 200 | bucketCount 201 | ), 202 | canvas, 203 | undefined, 204 | undefined, 205 | needsDegreeSymbol ? String.fromCharCode(176) : undefined 206 | ); 207 | histogramContainer.classList.add('histogram-container'); 208 | histogramLabel.classList.add('graph-label'); 209 | histogramLabel.textContent = prop + ' histogram'; 210 | histogramContainer.appendChild(canvas); 211 | histogramContainer.appendChild(histogramLabel); 212 | statgroup.appendChild(histogramContainer); 213 | section.appendChild(statgroup); 214 | } 215 | output.appendChild(section); 216 | } 217 | 218 | // Report 219 | //console.log(results); 220 | } 221 | 222 | function assembleOptions(heightmap, easing, smoothing, turbulent) { 223 | return { 224 | after: function(vertices, options) { 225 | applyEdgeFilter(vertices, options); 226 | }, 227 | easing: THREE.Terrain[easing || 'Linear'], 228 | heightmap: heightmap === 'heightmap.png' ? heightmapImage : THREE.Terrain[heightmap || 'PerlinDiamond'], 229 | material: mat, 230 | maxHeight: 100, 231 | minHeight: -100, 232 | steps: 1, 233 | stretch: true, 234 | turbulent: turbulent || false, 235 | xSize: 1024, 236 | ySize: 1024, 237 | xSegments: 63, 238 | ySegments: 63, 239 | }; 240 | } 241 | 242 | function applyEdgeFilter(vertices, options, edgeType, edgeDirection, edgeCurve) { 243 | if (!edgeDirection || edgeDirection === 'Normal') return; 244 | (!edgeType || edgeType === 'Box' ? THREE.Terrain.Edges : THREE.Terrain.RadialEdges)( 245 | vertices, 246 | options, 247 | edgeDirection === 'Up' ? true : false, 248 | edgeDistance || 256, 249 | THREE.Terrain[edgeCurve || 'EaseInOut'] 250 | ); 251 | } 252 | 253 | /** 254 | * Utility method to round numbers to a given number of decimal places. 255 | * 256 | * Usage: 257 | * 3.5.round(0) // 4 258 | * Math.random().round(4) // 0.8179 259 | * var a = 5532; a.round(-2) // 5500 260 | * Number.prototype.round(12345.6, -1) // 12350 261 | * 32..round(-1) // 30 (two dots required since the first one is a decimal) 262 | */ 263 | Number.prototype.round = function(v, a) { 264 | if (typeof a === 'undefined') { 265 | a = v; 266 | v = this; 267 | } 268 | if (!a) a = 0; 269 | var m = Math.pow(10, a|0); 270 | return Math.round(v*m)/m; 271 | }; 272 | 273 | /** 274 | * Put numbers into buckets that have equal-size ranges. 275 | * 276 | * @param {Number[]} data 277 | * The data to bucket. 278 | * @param {Number} bucketCount 279 | * The number of buckets to use. 280 | * @param {Number} [min] 281 | * The minimum allowed data value. Defaults to the smallest value passed. 282 | * @param {Number} [max] 283 | * The maximum allowed data value. Defaults to the largest value passed. 284 | * 285 | * @return {Number[][]} An array of buckets of numbers. 286 | */ 287 | function bucketNumbersLinearly(data, bucketCount, min, max) { 288 | var i = 0, 289 | l = data.length; 290 | // If min and max aren't given, set them to the highest and lowest data values 291 | if (typeof min === 'undefined') { 292 | min = Infinity; 293 | max = -Infinity; 294 | for (i = 0; i < l; i++) { 295 | if (data[i] < min) min = data[i]; 296 | if (data[i] > max) max = data[i]; 297 | } 298 | } 299 | var inc = (max - min) / bucketCount, 300 | buckets = new Array(bucketCount); 301 | // Initialize buckets 302 | for (i = 0; i < bucketCount; i++) { 303 | buckets[i] = []; 304 | } 305 | // Put the numbers into buckets 306 | for (i = 0; i < l; i++) { 307 | // Buckets include the lower bound but not the higher bound, except the top bucket 308 | try { 309 | if (data[i] === max) buckets[bucketCount-1].push(data[i]); 310 | else buckets[((data[i] - min) / inc) | 0].push(data[i]); 311 | } catch(e) { 312 | console.warn('Numbers in the data are outside of the min and max values used to bucket the data.'); 313 | } 314 | } 315 | return buckets; 316 | } 317 | 318 | /** 319 | * Draw a histogram. 320 | * 321 | * @param {Number[][]} buckets 322 | * An array of data to draw, typically from `bucketNumbersLinearly()`. 323 | * @param {HTMLCanvasElement} canvas 324 | * The canvas on which to draw the histogram. 325 | * @param {Number} [minV] 326 | * The lowest x-value to plot. Defaults to the lowest value in the data. 327 | * @param {Number} [maxV] 328 | * The highest x-value to plot. Defaults to the highest value in the data. 329 | * @param {String} [append=''] 330 | * A string to append to the bar labels. Defaults to the empty string. 331 | */ 332 | function drawHistogram(buckets, canvas, minV, maxV, append) { 333 | var context = canvas.getContext('2d'), 334 | width = 280, 335 | height = 180, 336 | border = 10, 337 | separator = 4, 338 | max = typeof maxV === 'undefined' ? -Infinity : maxV, 339 | min = typeof minV === 'undefined' ? Infinity : minV, 340 | i, 341 | l; 342 | canvas.width = width + border*2; 343 | canvas.height = height + border*2; 344 | if (typeof append === 'undefined') append = ''; 345 | 346 | // If max or min is not set, set them to the highest/lowest value. 347 | if (max === -Infinity || min === Infinity) { 348 | for (i = 0, l = buckets.length; i < l; i++) { 349 | for (var j = 0, m = buckets[i].length; j < m; j++) { 350 | if (buckets[i][j] > max) { 351 | max = buckets[i][j]; 352 | } 353 | if (buckets[i][j] < min) { 354 | min = buckets[i][j]; 355 | } 356 | } 357 | } 358 | } 359 | 360 | // Find the size of the largest bucket. 361 | var maxBucketSize = 0; 362 | for (i = 0, l = buckets.length; i < l; i++) { 363 | if (buckets[i].length > maxBucketSize) { 364 | maxBucketSize = buckets[i].length; 365 | } 366 | } 367 | 368 | // Draw a bar. 369 | var unitSizeY = (height - separator) / maxBucketSize, 370 | unitSizeX = (width - (buckets.length + 1) * separator) / buckets.length; 371 | if (unitSizeX >= 1) unitSizeX = Math.floor(unitSizeX); 372 | if (unitSizeY >= 1) unitSizeY = Math.floor(unitSizeY); 373 | context.fillStyle = 'rgba(13, 42, 64, 1)'; 374 | for (i = 0, l = buckets.length; i < l; i++) { 375 | context.fillRect( 376 | border + separator + i * (unitSizeX + separator), 377 | border + height - (separator + buckets[i].length * unitSizeY), 378 | unitSizeX, 379 | unitSizeY * buckets[i].length 380 | ); 381 | } 382 | 383 | // Draw the label text on the bar. 384 | context.fillStyle = 'rgba(144, 176, 192, 1)'; 385 | context.font = '12px Arial'; 386 | for (i = 0, l = buckets.length; i < l; i++) { 387 | var text = Math.floor(((i + 0.5) / buckets.length) * (max - min) + min) + '' + append; 388 | context.fillText( 389 | text, 390 | border + separator + i * (unitSizeX + separator) + Math.floor((unitSizeX - context.measureText(text).width) * 0.5), 391 | border + height - 8, 392 | unitSizeX 393 | ); 394 | } 395 | 396 | // Draw axes. 397 | context.strokeStyle = 'rgba(13, 42, 64, 1)'; 398 | context.lineWidth = 2; 399 | context.beginPath(); 400 | context.moveTo(border, border); 401 | context.lineTo(border, height + border); 402 | context.moveTo(border, height + border); 403 | context.lineTo(width + border, height + border); 404 | context.stroke(); 405 | } 406 | 407 | heightmapImage.src = 'images/heightmap.png'; 408 | -------------------------------------------------------------------------------- /statistics/styles/index.css: -------------------------------------------------------------------------------- 1 | ::-moz-selection { background: #39f; } 2 | ::selection { background: #39f; } 3 | html, body { 4 | background-color: #333333; 5 | color: #AAAAAA; 6 | font-family: Arial, sans-serif; 7 | font-size: 18px; 8 | letter-spacing: 0.03em; 9 | line-height: 1.4; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | h2 { 14 | background-color: #222222; 15 | clear: both; 16 | font-size: 27px; 17 | font-weight: bolder; 18 | margin: 10px 0 35px; 19 | padding: 10px 0; 20 | text-align: center; 21 | } 22 | canvas { 23 | background-color: rgb(239, 247, 255); 24 | border: 1px solid black; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | a, 29 | a:link, 30 | a:visited { 31 | color: #2A88CC; 32 | text-decoration: none; 33 | } 34 | a:hover, 35 | a:active { 36 | color: #2A88CC; 37 | text-decoration: underline; 38 | } 39 | #analytics { 40 | border: 1px solid #AAAAAA; 41 | padding: 20px 40px; 42 | } 43 | .section { 44 | border-top: 1px solid #AAAAAA; 45 | padding: 20px 0 50px; 46 | } 47 | .statgroup { 48 | display: inline-block; 49 | min-width: 400px; 50 | padding: 0 10px 20px; 51 | } 52 | .label { 53 | font-weight: bold; 54 | padding-right: 10px; 55 | } 56 | .label:after { 57 | content: ": "; 58 | } 59 | .value { 60 | font-family: monospace; 61 | white-space: pre; 62 | } 63 | .histogram-container { 64 | display: inline-block; 65 | margin-top: 10px; 66 | } 67 | .graph-label { 68 | font-style: italic; 69 | text-align: center; 70 | } 71 | .degree:after { 72 | content: "\00b0"; 73 | } 74 | --------------------------------------------------------------------------------