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 |