aboutsummaryrefslogtreecommitdiff
path: root/engine/src/terrain
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src/terrain')
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.frag76
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.j3md41
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.vert22
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/Terrain.frag63
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/Terrain.j3md33
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/Terrain.vert23
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.frag625
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.j3md254
-rw-r--r--engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.vert107
-rw-r--r--engine/src/terrain/com/jme3/terrain/GeoMap.java365
-rw-r--r--engine/src/terrain/com/jme3/terrain/ProgressMonitor.java68
-rw-r--r--engine/src/terrain/com/jme3/terrain/Terrain.java209
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/LODGeomap.java1110
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/LRUCache.java122
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/NormalRecalcControl.java106
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGrid.java514
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridListener.java47
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridTileLoader.java21
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainLodControl.java210
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java983
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java1862
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/UpdatedTerrainPatch.java183
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/grid/AssetTileLoader.java92
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/grid/FractalTileLoader.java87
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/grid/ImageTileLoader.java153
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/DistanceLodCalculator.java182
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculator.java65
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculatorFactory.java50
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodDistanceCalculatorFactory.java88
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodPerspectiveCalculatorFactory.java81
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodThreshold.java54
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/PerspectiveLodCalculator.java175
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/SimpleLodThreshold.java116
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/util/EntropyComputeUtil.java75
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamTerrainPicker.java243
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamYUpGridTracer.java193
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPickData.java79
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPicker.java56
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/AbstractHeightMap.java485
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/CombinerHeightMap.java269
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/FaultHeightMap.java326
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/FluidSimHeightMap.java308
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/HeightMap.java158
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/HeightMapGrid.java23
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/HillHeightMap.java261
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMap.java177
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMapGrid.java79
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/ImageHeightmap.java30
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/MidpointDisplacementHeightMap.java273
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/Namer.java21
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/ParticleDepositionHeightMap.java397
-rw-r--r--engine/src/terrain/com/jme3/terrain/heightmap/RawHeightMap.java248
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/Basis.java77
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/Color.java134
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/Filter.java44
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/ShaderUtils.java288
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/basis/FilteredBasis.java111
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/basis/ImprovedNoise.java127
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/basis/Noise.java94
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/basis/NoiseAggregator.java64
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/AbstractFilter.java100
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/HydraulicErodeFilter.java154
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/IterativeFilter.java102
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/OptimizedErode.java113
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/PerturbFilter.java98
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/SmoothFilter.java80
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/filter/ThermalErodeFilter.java101
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/fractal/Fractal.java57
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/fractal/FractalSum.java142
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/modulator/CatRom2.java78
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/modulator/Modulator.java36
-rw-r--r--engine/src/terrain/com/jme3/terrain/noise/modulator/NoiseModulator.java34
72 files changed, 13922 insertions, 0 deletions
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.frag b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.frag
new file mode 100644
index 0000000..b0b594c
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.frag
@@ -0,0 +1,76 @@
+uniform vec3 m_region1;
+uniform vec3 m_region2;
+uniform vec3 m_region3;
+uniform vec3 m_region4;
+
+uniform sampler2D m_region1ColorMap;
+uniform sampler2D m_region2ColorMap;
+uniform sampler2D m_region3ColorMap;
+uniform sampler2D m_region4ColorMap;
+uniform sampler2D m_slopeColorMap;
+
+uniform float m_slopeTileFactor;
+uniform float m_terrainSize;
+
+varying vec3 normal;
+varying vec4 position;
+
+vec4 GenerateTerrainColor() {
+ float height = position.y;
+ vec4 p = position / m_terrainSize;
+
+ vec3 blend = abs( normal );
+ blend = (blend -0.2) * 0.7;
+ blend = normalize(max(blend, 0.00001)); // Force weights to sum to 1.0 (very important!)
+ float b = (blend.x + blend.y + blend.z);
+ blend /= vec3(b, b, b);
+
+ vec4 terrainColor = vec4(0.0, 0.0, 0.0, 1.0);
+
+ float m_regionMin = 0.0;
+ float m_regionMax = 0.0;
+ float m_regionRange = 0.0;
+ float m_regionWeight = 0.0;
+
+ vec4 slopeCol1 = texture2D(m_slopeColorMap, p.yz * m_slopeTileFactor);
+ vec4 slopeCol2 = texture2D(m_slopeColorMap, p.xy * m_slopeTileFactor);
+
+ // Terrain m_region 1.
+ m_regionMin = m_region1.x;
+ m_regionMax = m_region1.y;
+ m_regionRange = m_regionMax - m_regionMin;
+ m_regionWeight = (m_regionRange - abs(height - m_regionMax)) / m_regionRange;
+ m_regionWeight = max(0.0, m_regionWeight);
+ terrainColor += m_regionWeight * texture2D(m_region1ColorMap, p.xz * m_region1.z);
+
+ // Terrain m_region 2.
+ m_regionMin = m_region2.x;
+ m_regionMax = m_region2.y;
+ m_regionRange = m_regionMax - m_regionMin;
+ m_regionWeight = (m_regionRange - abs(height - m_regionMax)) / m_regionRange;
+ m_regionWeight = max(0.0, m_regionWeight);
+ terrainColor += m_regionWeight * (texture2D(m_region2ColorMap, p.xz * m_region2.z));
+
+ // Terrain m_region 3.
+ m_regionMin = m_region3.x;
+ m_regionMax = m_region3.y;
+ m_regionRange = m_regionMax - m_regionMin;
+ m_regionWeight = (m_regionRange - abs(height - m_regionMax)) / m_regionRange;
+ m_regionWeight = max(0.0, m_regionWeight);
+ terrainColor += m_regionWeight * texture2D(m_region3ColorMap, p.xz * m_region3.z);
+
+ // Terrain m_region 4.
+ m_regionMin = m_region4.x;
+ m_regionMax = m_region4.y;
+ m_regionRange = m_regionMax - m_regionMin;
+ m_regionWeight = (m_regionRange - abs(height - m_regionMax)) / m_regionRange;
+ m_regionWeight = max(0.0, m_regionWeight);
+ terrainColor += m_regionWeight * texture2D(m_region4ColorMap, p.xz * m_region4.z);
+
+ return (blend.y * terrainColor + blend.x * slopeCol1 + blend.z * slopeCol2);
+}
+
+void main() {
+ vec4 color = GenerateTerrainColor();
+ gl_FragColor = color;
+}
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.j3md b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.j3md
new file mode 100644
index 0000000..4b836f0
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.j3md
@@ -0,0 +1,41 @@
+MaterialDef Terrain {
+
+ // Parameters to material:
+ // regionXColorMap: X = 1..4 the texture that should be appliad to state X
+ // regionX: a Vector3f containing the following information:
+ // regionX.x: the start height of the region
+ // regionX.y: the end height of the region
+ // regionX.z: the texture scale for the region
+ // it might not be the most elegant way for storing these 3 values, but it packs the data nicely :)
+ // slopeColorMap: the texture to be used for cliffs, and steep mountain sites
+ // slopeTileFactor: the texture scale for slopes
+ // terrainSize: the total size of the terrain (used for scaling the texture)
+ MaterialParameters {
+ Texture2D region1ColorMap
+ Texture2D region2ColorMap
+ Texture2D region3ColorMap
+ Texture2D region4ColorMap
+ Texture2D slopeColorMap
+ Float slopeTileFactor
+ Float terrainSize
+ Vector3 region1
+ Vector3 region2
+ Vector3 region3
+ Vector3 region4
+ }
+
+ Technique {
+ VertexShader GLSL100: Common/MatDefs/Terrain/HeightBasedTerrain.vert
+ FragmentShader GLSL100: Common/MatDefs/Terrain/HeightBasedTerrain.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ NormalMatrix
+ }
+ }
+
+ Technique FixedFunc {
+ }
+
+} \ No newline at end of file
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.vert b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.vert
new file mode 100644
index 0000000..8260d8f
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/HeightBasedTerrain.vert
@@ -0,0 +1,22 @@
+uniform float m_tilingFactor;
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform mat3 g_NormalMatrix;
+
+uniform float m_terrainSize;
+
+attribute vec4 inTexCoord;
+attribute vec3 inNormal;
+attribute vec3 inPosition;
+
+varying vec3 normal;
+varying vec4 position;
+
+void main()
+{
+ normal = normalize(inNormal);
+ position = g_WorldMatrix * vec4(inPosition, 0.0);
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1);
+}
+
+
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/Terrain.frag b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.frag
new file mode 100644
index 0000000..7ae56cb
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.frag
@@ -0,0 +1,63 @@
+uniform sampler2D m_Alpha;
+uniform sampler2D m_Tex1;
+uniform sampler2D m_Tex2;
+uniform sampler2D m_Tex3;
+uniform float m_Tex1Scale;
+uniform float m_Tex2Scale;
+uniform float m_Tex3Scale;
+
+varying vec2 texCoord;
+
+#ifdef TRI_PLANAR_MAPPING
+ varying vec4 vVertex;
+ varying vec3 vNormal;
+#endif
+
+void main(void)
+{
+
+ // get the alpha value at this 2D texture coord
+ vec4 alpha = texture2D( m_Alpha, texCoord.xy );
+
+#ifdef TRI_PLANAR_MAPPING
+ // tri-planar texture bending factor for this fragment's normal
+ vec3 blending = abs( vNormal );
+ blending = (blending -0.2) * 0.7;
+ blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0 (very important!)
+ float b = (blending.x + blending.y + blending.z);
+ blending /= vec3(b, b, b);
+
+ // texture coords
+ vec4 coords = vVertex;
+
+ vec4 col1 = texture2D( m_Tex1, coords.yz * m_Tex1Scale );
+ vec4 col2 = texture2D( m_Tex1, coords.xz * m_Tex1Scale );
+ vec4 col3 = texture2D( m_Tex1, coords.xy * m_Tex1Scale );
+ // blend the results of the 3 planar projections.
+ vec4 tex1 = col1 * blending.x + col2 * blending.y + col3 * blending.z;
+
+ col1 = texture2D( m_Tex2, coords.yz * m_Tex2Scale );
+ col2 = texture2D( m_Tex2, coords.xz * m_Tex2Scale );
+ col3 = texture2D( m_Tex2, coords.xy * m_Tex2Scale );
+ // blend the results of the 3 planar projections.
+ vec4 tex2 = col1 * blending.x + col2 * blending.y + col3 * blending.z;
+
+ col1 = texture2D( m_Tex3, coords.yz * m_Tex3Scale );
+ col2 = texture2D( m_Tex3, coords.xz * m_Tex3Scale );
+ col3 = texture2D( m_Tex3, coords.xy * m_Tex3Scale );
+ // blend the results of the 3 planar projections.
+ vec4 tex3 = col1 * blending.x + col2 * blending.y + col3 * blending.z;
+
+#else
+ vec4 tex1 = texture2D( m_Tex1, texCoord.xy * m_Tex1Scale ); // Tile
+ vec4 tex2 = texture2D( m_Tex2, texCoord.xy * m_Tex2Scale ); // Tile
+ vec4 tex3 = texture2D( m_Tex3, texCoord.xy * m_Tex3Scale ); // Tile
+
+#endif
+
+ vec4 outColor = tex1 * alpha.r; // Red channel
+ outColor = mix( outColor, tex2, alpha.g ); // Green channel
+ outColor = mix( outColor, tex3, alpha.b ); // Blue channel
+ gl_FragColor = outColor;
+}
+
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/Terrain.j3md b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.j3md
new file mode 100644
index 0000000..152f511
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.j3md
@@ -0,0 +1,33 @@
+MaterialDef Terrain {
+
+ MaterialParameters {
+
+ // use tri-planar mapping
+ Boolean useTriPlanarMapping
+
+ Texture2D Alpha
+ Texture2D Tex1
+ Texture2D Tex2
+ Texture2D Tex3
+ Float Tex1Scale
+ Float Tex2Scale
+ Float Tex3Scale
+ }
+
+ Technique {
+ VertexShader GLSL100: Common/MatDefs/Terrain/Terrain.vert
+ FragmentShader GLSL100: Common/MatDefs/Terrain/Terrain.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ Defines {
+ TRI_PLANAR_MAPPING : useTriPlanarMapping
+ }
+ }
+
+ Technique FixedFunc {
+ }
+
+} \ No newline at end of file
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/Terrain.vert b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.vert
new file mode 100644
index 0000000..ddb40a9
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/Terrain.vert
@@ -0,0 +1,23 @@
+uniform mat4 g_WorldViewProjectionMatrix;
+
+attribute vec3 inPosition;
+attribute vec3 inNormal;
+attribute vec2 inTexCoord;
+
+varying vec2 texCoord;
+
+#ifdef TRI_PLANAR_MAPPING
+ varying vec4 vVertex;
+ varying vec3 vNormal;
+#endif
+
+void main(){
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+ texCoord = inTexCoord;
+
+#ifdef TRI_PLANAR_MAPPING
+ vVertex = vec4(inPosition,0.0);
+ vNormal = inNormal;
+#endif
+
+} \ No newline at end of file
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.frag b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.frag
new file mode 100644
index 0000000..4d1d41a
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.frag
@@ -0,0 +1,625 @@
+
+uniform float m_Shininess;
+uniform vec4 g_LightDirection;
+
+varying vec4 AmbientSum;
+varying vec4 DiffuseSum;
+varying vec4 SpecularSum;
+
+varying vec3 vNormal;
+varying vec2 texCoord;
+varying vec3 vPosition;
+varying vec3 vnPosition;
+varying vec3 vViewDir;
+varying vec4 vLightDir;
+varying vec4 vnLightDir;
+varying vec3 lightVec;
+
+
+#ifdef DIFFUSEMAP
+ uniform sampler2D m_DiffuseMap;
+#endif
+#ifdef DIFFUSEMAP_1
+ uniform sampler2D m_DiffuseMap_1;
+#endif
+#ifdef DIFFUSEMAP_2
+ uniform sampler2D m_DiffuseMap_2;
+#endif
+#ifdef DIFFUSEMAP_3
+ uniform sampler2D m_DiffuseMap_3;
+#endif
+#ifdef DIFFUSEMAP_4
+ uniform sampler2D m_DiffuseMap_4;
+#endif
+#ifdef DIFFUSEMAP_5
+ uniform sampler2D m_DiffuseMap_5;
+#endif
+#ifdef DIFFUSEMAP_6
+ uniform sampler2D m_DiffuseMap_6;
+#endif
+#ifdef DIFFUSEMAP_7
+ uniform sampler2D m_DiffuseMap_7;
+#endif
+#ifdef DIFFUSEMAP_8
+ uniform sampler2D m_DiffuseMap_8;
+#endif
+#ifdef DIFFUSEMAP_9
+ uniform sampler2D m_DiffuseMap_9;
+#endif
+#ifdef DIFFUSEMAP_10
+ uniform sampler2D m_DiffuseMap_10;
+#endif
+#ifdef DIFFUSEMAP_11
+ uniform sampler2D m_DiffuseMap_11;
+#endif
+
+
+#ifdef DIFFUSEMAP_0_SCALE
+ uniform float m_DiffuseMap_0_scale;
+#endif
+#ifdef DIFFUSEMAP_1_SCALE
+ uniform float m_DiffuseMap_1_scale;
+#endif
+#ifdef DIFFUSEMAP_2_SCALE
+ uniform float m_DiffuseMap_2_scale;
+#endif
+#ifdef DIFFUSEMAP_3_SCALE
+ uniform float m_DiffuseMap_3_scale;
+#endif
+#ifdef DIFFUSEMAP_4_SCALE
+ uniform float m_DiffuseMap_4_scale;
+#endif
+#ifdef DIFFUSEMAP_5_SCALE
+ uniform float m_DiffuseMap_5_scale;
+#endif
+#ifdef DIFFUSEMAP_6_SCALE
+ uniform float m_DiffuseMap_6_scale;
+#endif
+#ifdef DIFFUSEMAP_7_SCALE
+ uniform float m_DiffuseMap_7_scale;
+#endif
+#ifdef DIFFUSEMAP_8_SCALE
+ uniform float m_DiffuseMap_8_scale;
+#endif
+#ifdef DIFFUSEMAP_9_SCALE
+ uniform float m_DiffuseMap_9_scale;
+#endif
+#ifdef DIFFUSEMAP_10_SCALE
+ uniform float m_DiffuseMap_10_scale;
+#endif
+#ifdef DIFFUSEMAP_11_SCALE
+ uniform float m_DiffuseMap_11_scale;
+#endif
+
+
+#ifdef ALPHAMAP
+ uniform sampler2D m_AlphaMap;
+#endif
+#ifdef ALPHAMAP_1
+ uniform sampler2D m_AlphaMap_1;
+#endif
+#ifdef ALPHAMAP_2
+ uniform sampler2D m_AlphaMap_2;
+#endif
+
+#ifdef NORMALMAP
+ uniform sampler2D m_NormalMap;
+#endif
+#ifdef NORMALMAP_1
+ uniform sampler2D m_NormalMap_1;
+#endif
+#ifdef NORMALMAP_2
+ uniform sampler2D m_NormalMap_2;
+#endif
+#ifdef NORMALMAP_3
+ uniform sampler2D m_NormalMap_3;
+#endif
+#ifdef NORMALMAP_4
+ uniform sampler2D m_NormalMap_4;
+#endif
+#ifdef NORMALMAP_5
+ uniform sampler2D m_NormalMap_5;
+#endif
+#ifdef NORMALMAP_6
+ uniform sampler2D m_NormalMap_6;
+#endif
+#ifdef NORMALMAP_7
+ uniform sampler2D m_NormalMap_7;
+#endif
+#ifdef NORMALMAP_8
+ uniform sampler2D m_NormalMap_8;
+#endif
+#ifdef NORMALMAP_9
+ uniform sampler2D m_NormalMap_9;
+#endif
+#ifdef NORMALMAP_10
+ uniform sampler2D m_NormalMap_10;
+#endif
+#ifdef NORMALMAP_11
+ uniform sampler2D m_NormalMap_11;
+#endif
+
+
+#ifdef TRI_PLANAR_MAPPING
+ varying vec4 wVertex;
+ varying vec3 wNormal;
+#endif
+
+
+
+float tangDot(in vec3 v1, in vec3 v2){
+ float d = dot(v1,v2);
+ #ifdef V_TANGENT
+ d = 1.0 - d*d;
+ return step(0.0, d) * sqrt(d);
+ #else
+ return d;
+ #endif
+}
+
+
+float lightComputeDiffuse(in vec3 norm, in vec3 lightdir, in vec3 viewdir){
+ return max(0.0, dot(norm, lightdir));
+}
+
+float lightComputeSpecular(in vec3 norm, in vec3 viewdir, in vec3 lightdir, in float shiny){
+ #ifdef WARDISO
+ // Isotropic Ward
+ vec3 halfVec = normalize(viewdir + lightdir);
+ float NdotH = max(0.001, tangDot(norm, halfVec));
+ float NdotV = max(0.001, tangDot(norm, viewdir));
+ float NdotL = max(0.001, tangDot(norm, lightdir));
+ float a = tan(acos(NdotH));
+ float p = max(shiny/128.0, 0.001);
+ return NdotL * (1.0 / (4.0*3.14159265*p*p)) * (exp(-(a*a)/(p*p)) / (sqrt(NdotV * NdotL)));
+ #else
+ // Standard Phong
+ vec3 R = reflect(-lightdir, norm);
+ return pow(max(tangDot(R, viewdir), 0.0), shiny);
+ #endif
+}
+
+vec2 computeLighting(in vec3 wvPos, in vec3 wvNorm, in vec3 wvViewDir, in vec3 wvLightDir){
+ float diffuseFactor = lightComputeDiffuse(wvNorm, wvLightDir, wvViewDir);
+ float specularFactor = lightComputeSpecular(wvNorm, wvViewDir, wvLightDir, m_Shininess);
+ specularFactor *= step(1.0, m_Shininess);
+
+ float att = vLightDir.w;
+
+ return vec2(diffuseFactor, specularFactor) * vec2(att);
+}
+
+
+#ifdef ALPHAMAP
+
+ vec4 calculateDiffuseBlend(in vec2 texCoord) {
+ vec4 alphaBlend = texture2D( m_AlphaMap, texCoord.xy );
+
+ #ifdef ALPHAMAP_1
+ vec4 alphaBlend1 = texture2D( m_AlphaMap_1, texCoord.xy );
+ #endif
+ #ifdef ALPHAMAP_2
+ vec4 alphaBlend2 = texture2D( m_AlphaMap_2, texCoord.xy );
+ #endif
+
+ vec4 diffuseColor = texture2D(m_DiffuseMap, texCoord * m_DiffuseMap_0_scale);
+ diffuseColor *= alphaBlend.r;
+ #ifdef DIFFUSEMAP_1
+ vec4 diffuseColor1 = texture2D(m_DiffuseMap_1, texCoord * m_DiffuseMap_1_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor1, alphaBlend.g );
+ #ifdef DIFFUSEMAP_2
+ vec4 diffuseColor2 = texture2D(m_DiffuseMap_2, texCoord * m_DiffuseMap_2_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor2, alphaBlend.b );
+ #ifdef DIFFUSEMAP_3
+ vec4 diffuseColor3 = texture2D(m_DiffuseMap_3, texCoord * m_DiffuseMap_3_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor3, alphaBlend.a );
+ #ifdef ALPHAMAP_1
+ #ifdef DIFFUSEMAP_4
+ vec4 diffuseColor4 = texture2D(m_DiffuseMap_4, texCoord * m_DiffuseMap_4_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor4, alphaBlend1.r );
+ #ifdef DIFFUSEMAP_5
+ vec4 diffuseColor5 = texture2D(m_DiffuseMap_5, texCoord * m_DiffuseMap_5_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor5, alphaBlend1.g );
+ #ifdef DIFFUSEMAP_6
+ vec4 diffuseColor6 = texture2D(m_DiffuseMap_6, texCoord * m_DiffuseMap_6_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor6, alphaBlend1.b );
+ #ifdef DIFFUSEMAP_7
+ vec4 diffuseColor7 = texture2D(m_DiffuseMap_7, texCoord * m_DiffuseMap_7_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor7, alphaBlend1.a );
+ #ifdef ALPHAMAP_2
+ #ifdef DIFFUSEMAP_8
+ vec4 diffuseColor8 = texture2D(m_DiffuseMap_8, texCoord * m_DiffuseMap_8_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor8, alphaBlend2.r );
+ #ifdef DIFFUSEMAP_9
+ vec4 diffuseColor9 = texture2D(m_DiffuseMap_9, texCoord * m_DiffuseMap_9_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor9, alphaBlend2.g );
+ #ifdef DIFFUSEMAP_10
+ vec4 diffuseColor10 = texture2D(m_DiffuseMap_10, texCoord * m_DiffuseMap_10_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor10, alphaBlend2.b );
+ #ifdef DIFFUSEMAP_11
+ vec4 diffuseColor11 = texture2D(m_DiffuseMap_11, texCoord * m_DiffuseMap_11_scale);
+ diffuseColor = mix( diffuseColor, diffuseColor11, alphaBlend2.a );
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ return diffuseColor;
+ }
+
+ vec3 calculateNormal(in vec2 texCoord) {
+ vec3 normal = vec3(0,0,1);
+ vec3 n = vec3(0,0,0);
+
+ vec4 alphaBlend = texture2D( m_AlphaMap, texCoord.xy );
+
+ #ifdef ALPHAMAP_1
+ vec4 alphaBlend1 = texture2D( m_AlphaMap_1, texCoord.xy );
+ #endif
+ #ifdef ALPHAMAP_2
+ vec4 alphaBlend2 = texture2D( m_AlphaMap_2, texCoord.xy );
+ #endif
+
+ #ifdef NORMALMAP
+ n = texture2D(m_NormalMap, texCoord * m_DiffuseMap_0_scale).xyz;
+ normal += n * alphaBlend.r;
+ #endif
+
+ #ifdef NORMALMAP_1
+ n = texture2D(m_NormalMap_1, texCoord * m_DiffuseMap_1_scale).xyz;
+ normal += n * alphaBlend.g;
+ #endif
+
+ #ifdef NORMALMAP_2
+ n = texture2D(m_NormalMap_2, texCoord * m_DiffuseMap_2_scale).xyz;
+ normal += n * alphaBlend.b;
+ #endif
+
+ #ifdef NORMALMAP_3
+ n = texture2D(m_NormalMap_3, texCoord * m_DiffuseMap_3_scale).xyz;
+ normal += n * alphaBlend.a;
+ #endif
+
+ #ifdef ALPHAMAP_1
+ #ifdef NORMALMAP_4
+ n = texture2D(m_NormalMap_4, texCoord * m_DiffuseMap_4_scale).xyz;
+ normal += n * alphaBlend1.r;
+ #endif
+
+ #ifdef NORMALMAP_5
+ n = texture2D(m_NormalMap_5, texCoord * m_DiffuseMap_5_scale).xyz;
+ normal += n * alphaBlend1.g;
+ #endif
+
+ #ifdef NORMALMAP_6
+ n = texture2D(m_NormalMap_6, texCoord * m_DiffuseMap_6_scale).xyz;
+ normal += n * alphaBlend1.b;
+ #endif
+
+ #ifdef NORMALMAP_7
+ n = texture2D(m_NormalMap_7, texCoord * m_DiffuseMap_7_scale).xyz;
+ normal += n * alphaBlend1.a;
+ #endif
+ #endif
+
+ #ifdef ALPHAMAP_2
+ #ifdef NORMALMAP_8
+ n = texture2D(m_NormalMap_8, texCoord * m_DiffuseMap_8_scale).xyz;
+ normal += n * alphaBlend2.r;
+ #endif
+
+ #ifdef NORMALMAP_9
+ n = texture2D(m_NormalMap_9, texCoord * m_DiffuseMap_9_scale);
+ normal += n * alphaBlend2.g;
+ #endif
+
+ #ifdef NORMALMAP_10
+ n = texture2D(m_NormalMap_10, texCoord * m_DiffuseMap_10_scale);
+ normal += n * alphaBlend2.b;
+ #endif
+
+ #ifdef NORMALMAP_11
+ n = texture2D(m_NormalMap_11, texCoord * m_DiffuseMap_11_scale);
+ normal += n * alphaBlend2.a;
+ #endif
+ #endif
+
+ normal = (normal.xyz * vec3(2.0) - vec3(1.0));
+ return normalize(normal);
+ }
+
+ #ifdef TRI_PLANAR_MAPPING
+
+ vec4 getTriPlanarBlend(in vec4 coords, in vec3 blending, in sampler2D map, in float scale) {
+ vec4 col1 = texture2D( map, coords.yz * scale);
+ vec4 col2 = texture2D( map, coords.xz * scale);
+ vec4 col3 = texture2D( map, coords.xy * scale);
+ // blend the results of the 3 planar projections.
+ vec4 tex = col1 * blending.x + col2 * blending.y + col3 * blending.z;
+ return tex;
+ }
+
+ vec4 calculateTriPlanarDiffuseBlend(in vec3 wNorm, in vec4 wVert, in vec2 texCoord) {
+ // tri-planar texture bending factor for this fragment's normal
+ vec3 blending = abs( wNorm );
+ blending = (blending -0.2) * 0.7;
+ blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0 (very important!)
+ float b = (blending.x + blending.y + blending.z);
+ blending /= vec3(b, b, b);
+
+ // texture coords
+ vec4 coords = wVert;
+
+ // blend the results of the 3 planar projections.
+ vec4 tex0 = getTriPlanarBlend(coords, blending, m_DiffuseMap, m_DiffuseMap_0_scale);
+
+ #ifdef DIFFUSEMAP_1
+ // blend the results of the 3 planar projections.
+ vec4 tex1 = getTriPlanarBlend(coords, blending, m_DiffuseMap_1, m_DiffuseMap_1_scale);
+ #endif
+ #ifdef DIFFUSEMAP_2
+ // blend the results of the 3 planar projections.
+ vec4 tex2 = getTriPlanarBlend(coords, blending, m_DiffuseMap_2, m_DiffuseMap_2_scale);
+ #endif
+ #ifdef DIFFUSEMAP_3
+ // blend the results of the 3 planar projections.
+ vec4 tex3 = getTriPlanarBlend(coords, blending, m_DiffuseMap_3, m_DiffuseMap_3_scale);
+ #endif
+ #ifdef DIFFUSEMAP_4
+ // blend the results of the 3 planar projections.
+ vec4 tex4 = getTriPlanarBlend(coords, blending, m_DiffuseMap_4, m_DiffuseMap_4_scale);
+ #endif
+ #ifdef DIFFUSEMAP_5
+ // blend the results of the 3 planar projections.
+ vec4 tex5 = getTriPlanarBlend(coords, blending, m_DiffuseMap_5, m_DiffuseMap_5_scale);
+ #endif
+ #ifdef DIFFUSEMAP_6
+ // blend the results of the 3 planar projections.
+ vec4 tex6 = getTriPlanarBlend(coords, blending, m_DiffuseMap_6, m_DiffuseMap_6_scale);
+ #endif
+ #ifdef DIFFUSEMAP_7
+ // blend the results of the 3 planar projections.
+ vec4 tex7 = getTriPlanarBlend(coords, blending, m_DiffuseMap_7, m_DiffuseMap_7_scale);
+ #endif
+ #ifdef DIFFUSEMAP_8
+ // blend the results of the 3 planar projections.
+ vec4 tex8 = getTriPlanarBlend(coords, blending, m_DiffuseMap_8, m_DiffuseMap_8_scale);
+ #endif
+ #ifdef DIFFUSEMAP_9
+ // blend the results of the 3 planar projections.
+ vec4 tex9 = getTriPlanarBlend(coords, blending, m_DiffuseMap_9, m_DiffuseMap_9_scale);
+ #endif
+ #ifdef DIFFUSEMAP_10
+ // blend the results of the 3 planar projections.
+ vec4 tex10 = getTriPlanarBlend(coords, blending, m_DiffuseMap_10, m_DiffuseMap_10_scale);
+ #endif
+ #ifdef DIFFUSEMAP_11
+ // blend the results of the 3 planar projections.
+ vec4 tex11 = getTriPlanarBlend(coords, blending, m_DiffuseMap_11, m_DiffuseMap_11_scale);
+ #endif
+
+ vec4 alphaBlend = texture2D( m_AlphaMap, texCoord.xy );
+
+ #ifdef ALPHAMAP_1
+ vec4 alphaBlend1 = texture2D( m_AlphaMap_1, texCoord.xy );
+ #endif
+ #ifdef ALPHAMAP_2
+ vec4 alphaBlend2 = texture2D( m_AlphaMap_2, texCoord.xy );
+ #endif
+
+ vec4 diffuseColor = tex0 * alphaBlend.r;
+ #ifdef DIFFUSEMAP_1
+ diffuseColor = mix( diffuseColor, tex1, alphaBlend.g );
+ #ifdef DIFFUSEMAP_2
+ diffuseColor = mix( diffuseColor, tex2, alphaBlend.b );
+ #ifdef DIFFUSEMAP_3
+ diffuseColor = mix( diffuseColor, tex3, alphaBlend.a );
+ #ifdef ALPHAMAP_1
+ #ifdef DIFFUSEMAP_4
+ diffuseColor = mix( diffuseColor, tex4, alphaBlend1.r );
+ #ifdef DIFFUSEMAP_5
+ diffuseColor = mix( diffuseColor, tex5, alphaBlend1.g );
+ #ifdef DIFFUSEMAP_6
+ diffuseColor = mix( diffuseColor, tex6, alphaBlend1.b );
+ #ifdef DIFFUSEMAP_7
+ diffuseColor = mix( diffuseColor, tex7, alphaBlend1.a );
+ #ifdef ALPHAMAP_2
+ #ifdef DIFFUSEMAP_8
+ diffuseColor = mix( diffuseColor, tex8, alphaBlend2.r );
+ #ifdef DIFFUSEMAP_9
+ diffuseColor = mix( diffuseColor, tex9, alphaBlend2.g );
+ #ifdef DIFFUSEMAP_10
+ diffuseColor = mix( diffuseColor, tex10, alphaBlend2.b );
+ #ifdef DIFFUSEMAP_11
+ diffuseColor = mix( diffuseColor, tex11, alphaBlend2.a );
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+ #endif
+
+ return diffuseColor;
+ }
+
+ vec3 calculateNormalTriPlanar(in vec3 wNorm, in vec4 wVert,in vec2 texCoord) {
+ // tri-planar texture bending factor for this fragment's world-space normal
+ vec3 blending = abs( wNorm );
+ blending = (blending -0.2) * 0.7;
+ blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0 (very important!)
+ float b = (blending.x + blending.y + blending.z);
+ blending /= vec3(b, b, b);
+
+ // texture coords
+ vec4 coords = wVert;
+ vec4 alphaBlend = texture2D( m_AlphaMap, texCoord.xy );
+
+ #ifdef ALPHAMAP_1
+ vec4 alphaBlend1 = texture2D( m_AlphaMap_1, texCoord.xy );
+ #endif
+ #ifdef ALPHAMAP_2
+ vec4 alphaBlend2 = texture2D( m_AlphaMap_2, texCoord.xy );
+ #endif
+
+ vec3 normal = vec3(0,0,1);
+ vec3 n = vec3(0,0,0);
+
+ #ifdef NORMALMAP
+ n = getTriPlanarBlend(coords, blending, m_NormalMap, m_DiffuseMap_0_scale).xyz;
+ normal += n * alphaBlend.r;
+ #endif
+
+ #ifdef NORMALMAP_1
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_1, m_DiffuseMap_1_scale).xyz;
+ normal += n * alphaBlend.g;
+ #endif
+
+ #ifdef NORMALMAP_2
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_2, m_DiffuseMap_2_scale).xyz;
+ normal += n * alphaBlend.b;
+ #endif
+
+ #ifdef NORMALMAP_3
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_3, m_DiffuseMap_3_scale).xyz;
+ normal += n * alphaBlend.a;
+ #endif
+
+ #ifdef ALPHAMAP_1
+ #ifdef NORMALMAP_4
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_4, m_DiffuseMap_4_scale).xyz;
+ normal += n * alphaBlend1.r;
+ #endif
+
+ #ifdef NORMALMAP_5
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_5, m_DiffuseMap_5_scale).xyz;
+ normal += n * alphaBlend1.g;
+ #endif
+
+ #ifdef NORMALMAP_6
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_6, m_DiffuseMap_6_scale).xyz;
+ normal += n * alphaBlend1.b;
+ #endif
+
+ #ifdef NORMALMAP_7
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_7, m_DiffuseMap_7_scale).xyz;
+ normal += n * alphaBlend1.a;
+ #endif
+ #endif
+
+ #ifdef ALPHAMAP_2
+ #ifdef NORMALMAP_8
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_8, m_DiffuseMap_8_scale).xyz;
+ normal += n * alphaBlend2.r;
+ #endif
+
+ #ifdef NORMALMAP_9
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_9, m_DiffuseMap_9_scale).xyz;
+ normal += n * alphaBlend2.g;
+ #endif
+
+ #ifdef NORMALMAP_10
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_10, m_DiffuseMap_10_scale).xyz;
+ normal += n * alphaBlend2.b;
+ #endif
+
+ #ifdef NORMALMAP_11
+ n = getTriPlanarBlend(coords, blending, m_NormalMap_11, m_DiffuseMap_11_scale).xyz;
+ normal += n * alphaBlend2.a;
+ #endif
+ #endif
+
+ normal = (normal.xyz * vec3(2.0) - vec3(1.0));
+ return normalize(normal);
+ }
+ #endif
+
+#endif
+
+
+
+void main(){
+
+ //----------------------
+ // diffuse calculations
+ //----------------------
+ #ifdef DIFFUSEMAP
+ #ifdef ALPHAMAP
+ #ifdef TRI_PLANAR_MAPPING
+ vec4 diffuseColor = calculateTriPlanarDiffuseBlend(wNormal, wVertex, texCoord);
+ #else
+ vec4 diffuseColor = calculateDiffuseBlend(texCoord);
+ #endif
+ #else
+ vec4 diffuseColor = texture2D(m_DiffuseMap, texCoord);
+ #endif
+ #else
+ vec4 diffuseColor = vec4(1.0);
+ #endif
+
+ float spotFallOff = 1.0;
+ if(g_LightDirection.w!=0.0){
+ vec3 L=normalize(lightVec.xyz);
+ vec3 spotdir = normalize(g_LightDirection.xyz);
+ float curAngleCos = dot(-L, spotdir);
+ float innerAngleCos = floor(g_LightDirection.w) * 0.001;
+ float outerAngleCos = fract(g_LightDirection.w);
+ float innerMinusOuter = innerAngleCos - outerAngleCos;
+
+ spotFallOff = (curAngleCos - outerAngleCos) / innerMinusOuter;
+
+ if(spotFallOff <= 0.0){
+ gl_FragColor = AmbientSum * diffuseColor;
+ return;
+ }else{
+ spotFallOff = clamp(spotFallOff, 0.0, 1.0);
+ }
+ }
+
+ //---------------------
+ // normal calculations
+ //---------------------
+ #if defined(NORMALMAP) || defined(NORMALMAP_1) || defined(NORMALMAP_2) || defined(NORMALMAP_3) || defined(NORMALMAP_4) || defined(NORMALMAP_5) || defined(NORMALMAP_6) || defined(NORMALMAP_7) || defined(NORMALMAP_8) || defined(NORMALMAP_9) || defined(NORMALMAP_10) || defined(NORMALMAP_11)
+ #ifdef TRI_PLANAR_MAPPING
+ vec3 normal = calculateNormalTriPlanar(wNormal, wVertex, texCoord);
+ #else
+ vec3 normal = calculateNormal(texCoord);
+ #endif
+ #else
+ vec3 normal = vNormal;
+ #endif
+
+
+ //-----------------------
+ // lighting calculations
+ //-----------------------
+ vec4 lightDir = vLightDir;
+ lightDir.xyz = normalize(lightDir.xyz);
+
+ vec2 light = computeLighting(vPosition, normal, vViewDir.xyz, lightDir.xyz)*spotFallOff;
+
+ vec4 specularColor = vec4(1.0);
+
+ //--------------------------
+ // final color calculations
+ //--------------------------
+ gl_FragColor = AmbientSum * diffuseColor +
+ DiffuseSum * diffuseColor * light.x +
+ SpecularSum * specularColor * light.y;
+
+ //gl_FragColor.a = alpha;
+} \ No newline at end of file
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.j3md b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.j3md
new file mode 100644
index 0000000..0c48999
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.j3md
@@ -0,0 +1,254 @@
+// NOTE: Doesn't support OpenGL1
+MaterialDef Terrain Lighting {
+
+ MaterialParameters {
+
+ // use tri-planar mapping
+ Boolean useTriPlanarMapping
+
+ // Use ward specular instead of phong
+ Boolean WardIso
+
+ // Are we rendering TerrainGrid
+ Boolean isTerrainGrid
+
+ // Ambient color
+ Color Ambient
+
+ // Diffuse color
+ Color Diffuse
+
+ // Specular color
+ Color Specular
+
+ // Specular power/shininess
+ Float Shininess : 1
+
+ // Texture map #0
+ Texture2D DiffuseMap
+ Float DiffuseMap_0_scale
+ Texture2D NormalMap
+
+ // Texture map #1
+ Texture2D DiffuseMap_1
+ Float DiffuseMap_1_scale
+ Texture2D NormalMap_1
+
+ // Texture map #2
+ Texture2D DiffuseMap_2
+ Float DiffuseMap_2_scale
+ Texture2D NormalMap_2
+
+ // Texture map #3
+ Texture2D DiffuseMap_3
+ Float DiffuseMap_3_scale
+ Texture2D NormalMap_3
+
+ // Texture map #4
+ Texture2D DiffuseMap_4
+ Float DiffuseMap_4_scale
+ Texture2D NormalMap_4
+
+ // Texture map #5
+ Texture2D DiffuseMap_5
+ Float DiffuseMap_5_scale
+ Texture2D NormalMap_5
+
+ // Texture map #6
+ Texture2D DiffuseMap_6
+ Float DiffuseMap_6_scale
+ Texture2D NormalMap_6
+
+ // Texture map #7
+ Texture2D DiffuseMap_7
+ Float DiffuseMap_7_scale
+ Texture2D NormalMap_7
+
+ // Texture map #8
+ Texture2D DiffuseMap_8
+ Float DiffuseMap_8_scale
+ Texture2D NormalMap_8
+
+ // Texture map #9
+ Texture2D DiffuseMap_9
+ Float DiffuseMap_9_scale
+ Texture2D NormalMap_9
+
+ // Texture map #10
+ Texture2D DiffuseMap_10
+ Float DiffuseMap_10_scale
+ Texture2D NormalMap_10
+
+ // Texture map #11
+ Texture2D DiffuseMap_11
+ Float DiffuseMap_11_scale
+ Texture2D NormalMap_11
+
+
+ // Specular/gloss map
+ Texture2D SpecularMap
+
+
+ // Texture that specifies alpha values
+ Texture2D AlphaMap
+ Texture2D AlphaMap_1
+ Texture2D AlphaMap_2
+
+ // Texture of the glowing parts of the material
+ Texture2D GlowMap
+
+ // The glow color of the object
+ Color GlowColor
+ }
+
+ Technique {
+
+ LightMode MultiPass
+
+ VertexShader GLSL100: Common/MatDefs/Terrain/TerrainLighting.vert
+ FragmentShader GLSL100: Common/MatDefs/Terrain/TerrainLighting.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ NormalMatrix
+ WorldViewMatrix
+ ViewMatrix
+ }
+
+ Defines {
+ TRI_PLANAR_MAPPING : useTriPlanarMapping
+ TERRAIN_GRID : isTerrainGrid
+ WARDISO : WardIso
+
+ DIFFUSEMAP : DiffuseMap
+ DIFFUSEMAP_1 : DiffuseMap_1
+ DIFFUSEMAP_2 : DiffuseMap_2
+ DIFFUSEMAP_3 : DiffuseMap_3
+ DIFFUSEMAP_4 : DiffuseMap_4
+ DIFFUSEMAP_5 : DiffuseMap_5
+ DIFFUSEMAP_6 : DiffuseMap_6
+ DIFFUSEMAP_7 : DiffuseMap_7
+ DIFFUSEMAP_8 : DiffuseMap_8
+ DIFFUSEMAP_9 : DiffuseMap_9
+ DIFFUSEMAP_10 : DiffuseMap_10
+ DIFFUSEMAP_11 : DiffuseMap_11
+ NORMALMAP : NormalMap
+ NORMALMAP_1 : NormalMap_1
+ NORMALMAP_2 : NormalMap_2
+ NORMALMAP_3 : NormalMap_3
+ NORMALMAP_4 : NormalMap_4
+ NORMALMAP_5 : NormalMap_5
+ NORMALMAP_6 : NormalMap_6
+ NORMALMAP_7 : NormalMap_7
+ NORMALMAP_8 : NormalMap_8
+ NORMALMAP_9 : NormalMap_9
+ NORMALMAP_10 : NormalMap_10
+ NORMALMAP_11 : NormalMap_11
+ SPECULARMAP : SpecularMap
+ ALPHAMAP : AlphaMap
+ ALPHAMAP_1 : AlphaMap_1
+ ALPHAMAP_2 : AlphaMap_2
+ DIFFUSEMAP_0_SCALE : DiffuseMap_0_scale
+ DIFFUSEMAP_1_SCALE : DiffuseMap_1_scale
+ DIFFUSEMAP_2_SCALE : DiffuseMap_2_scale
+ DIFFUSEMAP_3_SCALE : DiffuseMap_3_scale
+ DIFFUSEMAP_4_SCALE : DiffuseMap_4_scale
+ DIFFUSEMAP_5_SCALE : DiffuseMap_5_scale
+ DIFFUSEMAP_6_SCALE : DiffuseMap_6_scale
+ DIFFUSEMAP_7_SCALE : DiffuseMap_7_scale
+ DIFFUSEMAP_8_SCALE : DiffuseMap_8_scale
+ DIFFUSEMAP_9_SCALE : DiffuseMap_9_scale
+ DIFFUSEMAP_10_SCALE : DiffuseMap_10_scale
+ DIFFUSEMAP_11_SCALE : DiffuseMap_11_scale
+ }
+ }
+
+ Technique PreShadow {
+
+ VertexShader GLSL100 : Common/MatDefs/Shadow/PreShadow.vert
+ FragmentShader GLSL100 : Common/MatDefs/Shadow/PreShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldViewMatrix
+ }
+
+ Defines {
+ DIFFUSEMAP_ALPHA : DiffuseMap
+ }
+
+ RenderState {
+ FaceCull Off
+ DepthTest On
+ DepthWrite On
+ PolyOffset 5 0
+ ColorWrite Off
+ }
+
+ }
+
+ Technique PreNormalPass {
+
+ VertexShader GLSL100 : Common/MatDefs/SSAO/normal.vert
+ FragmentShader GLSL100 : Common/MatDefs/SSAO/normal.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldViewMatrix
+ NormalMatrix
+ }
+
+ Defines {
+ DIFFUSEMAP_ALPHA : DiffuseMap
+ }
+
+ RenderState {
+
+ }
+
+ }
+
+ Technique GBuf {
+
+ VertexShader GLSL100: Common/MatDefs/Light/GBuf.vert
+ FragmentShader GLSL100: Common/MatDefs/Light/GBuf.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ }
+
+ Defines {
+ VERTEX_COLOR : UseVertexColor
+ MATERIAL_COLORS : UseMaterialColors
+ V_TANGENT : VTangent
+ MINNAERT : Minnaert
+ WARDISO : WardIso
+
+ DIFFUSEMAP : DiffuseMap
+ NORMALMAP : NormalMap
+ SPECULARMAP : SpecularMap
+ PARALLAXMAP : ParallaxMap
+ }
+ }
+
+ Technique FixedFunc {
+ LightMode FixedPipeline
+ }
+
+ Technique Glow {
+
+ VertexShader GLSL100: Common/MatDefs/Misc/SimpleTextured.vert
+ FragmentShader GLSL100: Common/MatDefs/Light/Glow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ Defines {
+ HAS_GLOWMAP : GlowMap
+ HAS_GLOWCOLOR : GlowColor
+ }
+ }
+
+} \ No newline at end of file
diff --git a/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.vert b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.vert
new file mode 100644
index 0000000..a3a1cc2
--- /dev/null
+++ b/engine/src/terrain/Common/MatDefs/Terrain/TerrainLighting.vert
@@ -0,0 +1,107 @@
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldViewMatrix;
+uniform mat3 g_NormalMatrix;
+uniform mat4 g_ViewMatrix;
+
+uniform vec4 g_LightColor;
+uniform vec4 g_LightPosition;
+uniform vec4 g_AmbientLightColor;
+
+uniform float m_Shininess;
+
+attribute vec3 inPosition;
+attribute vec3 inNormal;
+attribute vec2 inTexCoord;
+attribute vec4 inTangent;
+
+varying vec3 vNormal;
+varying vec2 texCoord;
+varying vec3 vPosition;
+varying vec3 vnPosition;
+varying vec3 vViewDir;
+varying vec3 vnViewDir;
+varying vec4 vLightDir;
+varying vec4 vnLightDir;
+
+varying vec3 lightVec;
+
+varying vec4 AmbientSum;
+varying vec4 DiffuseSum;
+varying vec4 SpecularSum;
+
+#ifdef TRI_PLANAR_MAPPING
+ varying vec4 wVertex;
+ varying vec3 wNormal;
+#endif
+
+// JME3 lights in world space
+void lightComputeDir(in vec3 worldPos, in vec4 color, in vec4 position, out vec4 lightDir){
+ float posLight = step(0.5, color.w);
+ vec3 tempVec = position.xyz * sign(posLight - 0.5) - (worldPos * posLight);
+ lightVec.xyz = tempVec;
+ float dist = length(tempVec);
+ lightDir.w = clamp(1.0 - position.w * dist * posLight, 0.0, 1.0);
+ lightDir.xyz = tempVec / vec3(dist);
+}
+
+
+void main(){
+ vec4 pos = vec4(inPosition, 1.0);
+ gl_Position = g_WorldViewProjectionMatrix * pos;
+ #ifdef TERRAIN_GRID
+ texCoord = inTexCoord * 2.0;
+ #else
+ texCoord = inTexCoord;
+ #endif
+
+ vec3 wvPosition = (g_WorldViewMatrix * pos).xyz;
+ vec3 wvNormal = normalize(g_NormalMatrix * inNormal);
+ vec3 viewDir = normalize(-wvPosition);
+
+ vec4 wvLightPos = (g_ViewMatrix * vec4(g_LightPosition.xyz,clamp(g_LightColor.w,0.0,1.0)));
+ wvLightPos.w = g_LightPosition.w;
+ vec4 lightColor = g_LightColor;
+
+ //--------------------------
+ // specific to normal maps:
+ //--------------------------
+ #if defined(NORMALMAP) || defined(NORMALMAP_1) || defined(NORMALMAP_2) || defined(NORMALMAP_3) || defined(NORMALMAP_4) || defined(NORMALMAP_5) || defined(NORMALMAP_6) || defined(NORMALMAP_7) || defined(NORMALMAP_8) || defined(NORMALMAP_9) || defined(NORMALMAP_10) || defined(NORMALMAP_11)
+ vec3 wvTangent = normalize(g_NormalMatrix * inTangent.xyz);
+ vec3 wvBinormal = cross(wvNormal, wvTangent);
+
+ mat3 tbnMat = mat3(wvTangent, wvBinormal * -inTangent.w,wvNormal);
+
+ vPosition = wvPosition * tbnMat;
+ vViewDir = viewDir * tbnMat;
+ lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir);
+ vLightDir.xyz = (vLightDir.xyz * tbnMat).xyz;
+ #else
+
+ //-------------------------
+ // general to all lighting
+ //-------------------------
+ vNormal = wvNormal;
+
+ vPosition = wvPosition;
+ vViewDir = viewDir;
+
+ lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir);
+
+ #endif
+
+ //computing spot direction in view space and unpacking spotlight cos
+ // spotVec=(g_ViewMatrix *vec4(g_LightDirection.xyz,0.0) );
+ // spotVec.w=floor(g_LightDirection.w)*0.001;
+ // lightVec.w = fract(g_LightDirection.w);
+
+ AmbientSum = vec4(0.2, 0.2, 0.2, 1.0) * g_AmbientLightColor; // Default: ambient color is dark gray
+ DiffuseSum = lightColor;
+ SpecularSum = lightColor;
+
+
+#ifdef TRI_PLANAR_MAPPING
+ wVertex = vec4(inPosition,0.0);
+ wNormal = inNormal;
+#endif
+
+} \ No newline at end of file
diff --git a/engine/src/terrain/com/jme3/terrain/GeoMap.java b/engine/src/terrain/com/jme3/terrain/GeoMap.java
new file mode 100644
index 0000000..dc9264f
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/GeoMap.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain;
+
+import com.jme3.export.*;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+/**
+ * Constructs heightfields to be used in Terrain.
+ */
+public class GeoMap implements Savable {
+
+ protected float[] hdata;
+ protected int width, height, maxval;
+
+ public GeoMap() {}
+
+ @Deprecated
+ public GeoMap(FloatBuffer heightData, int width, int height, int maxval){
+ hdata = new float[heightData.limit()];
+ heightData.get(hdata);
+ this.width = width;
+ this.height = height;
+ this.maxval = maxval;
+ }
+
+ public GeoMap(float[] heightData, int width, int height, int maxval){
+ this.hdata = heightData;
+ this.width = width;
+ this.height = height;
+ this.maxval = maxval;
+ }
+
+ @Deprecated
+ public FloatBuffer getHeightData(){
+ if (!isLoaded())
+ return null;
+ return BufferUtils.createFloatBuffer(hdata);
+ }
+
+ public float[] getHeightArray(){
+ if (!isLoaded())
+ return null;
+ return hdata;
+ }
+
+ /**
+ * @return The maximum possible value that <code>getValue()</code> can
+ * return. Mostly depends on the source data format (byte, short, int, etc).
+ */
+ public int getMaximumValue(){
+ return maxval;
+ }
+
+ /**
+ * Returns the height value for a given point.
+ *
+ * MUST return the same value as getHeight(y*getWidth()+x)
+ *
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @returns an arbitrary height looked up from the heightmap
+ *
+ * @throws NullPointerException If isLoaded() is false
+ */
+ public float getValue(int x, int y) {
+ return hdata[y*width+x];
+ }
+
+ /**
+ * Returns the height value at the given index.
+ *
+ * zero index is top left of map,
+ * getWidth()*getHeight() index is lower right
+ *
+ * @param i The index
+ * @returns an arbitrary height looked up from the heightmap
+ *
+ * @throws NullPointerException If isLoaded() is false
+ */
+ public float getValue(int i) {
+ return hdata[i];
+ }
+
+
+ /**
+ * Returns the width of this Geomap
+ *
+ * @returns the width of this Geomap
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * Returns the height of this Geomap
+ *
+ * @returns the height of this Geomap
+ */
+ public int getHeight() {
+ return height;
+ }
+
+ /**
+ * Returns true if the Geomap data is loaded in memory
+ * If false, then the data is unavailable- must be loaded with load()
+ * before the methods getHeight/getNormal can be used
+ *
+ * @returns wether the geomap data is loaded in system memory
+ */
+ public boolean isLoaded() {
+ return true;
+ }
+
+ /**
+ * Creates a normal array from the normal data in this Geomap
+ *
+ * @param store A preallocated FloatBuffer where to store the data (optional), size must be >= getWidth()*getHeight()*3
+ * @returns store, or a new FloatBuffer if store is null
+ *
+ * @throws NullPointerException If isLoaded() or hasNormalmap() is false
+ */
+ public FloatBuffer writeNormalArray(FloatBuffer store, Vector3f scale) {
+
+ if (store!=null){
+ if (store.remaining() < getWidth()*getHeight()*3)
+ throw new BufferUnderflowException();
+ }else{
+ store = BufferUtils.createFloatBuffer(getWidth()*getHeight()*3);
+ }
+ store.rewind();
+
+ Vector3f oppositePoint = new Vector3f();
+ Vector3f adjacentPoint = new Vector3f();
+ Vector3f rootPoint = new Vector3f();
+ Vector3f tempNorm = new Vector3f();
+ int normalIndex = 0;
+
+ for (int y = 0; y < getHeight(); y++) {
+ for (int x = 0; x < getWidth(); x++) {
+ rootPoint.set(x, getValue(x,y), y);
+ if (y == getHeight() - 1) {
+ if (x == getWidth() - 1) { // case #4 : last row, last col
+ // left cross up
+// adj = normalIndex - getWidth();
+// opp = normalIndex - 1;
+ adjacentPoint.set(x, getValue(x,y-1), y-1);
+ oppositePoint.set(x-1, getValue(x-1, y), y);
+ } else { // case #3 : last row, except for last col
+ // right cross up
+// adj = normalIndex + 1;
+// opp = normalIndex - getWidth();
+ adjacentPoint.set(x+1, getValue(x+1,y), y);
+ oppositePoint.set(x, getValue(x,y-1), y-1);
+ }
+ } else {
+ if (x == getWidth() - 1) { // case #2 : last column except for last row
+ // left cross down
+ adjacentPoint.set(x-1, getValue(x-1,y), y);
+ oppositePoint.set(x, getValue(x,y+1), y+1);
+// adj = normalIndex - 1;
+// opp = normalIndex + getWidth();
+ } else { // case #1 : most cases
+ // right cross down
+ adjacentPoint.set(x, getValue(x,y+1), y+1);
+ oppositePoint.set(x+1, getValue(x+1,y), y);
+// adj = normalIndex + getWidth();
+// opp = normalIndex + 1;
+ }
+ }
+
+
+
+ tempNorm.set(adjacentPoint).subtractLocal(rootPoint)
+ .crossLocal(oppositePoint.subtractLocal(rootPoint));
+ tempNorm.multLocal(scale).normalizeLocal();
+// store.put(tempNorm.x).put(tempNorm.y).put(tempNorm.z);
+ BufferUtils.setInBuffer(tempNorm, store,
+ normalIndex);
+ normalIndex++;
+ }
+ }
+
+ return store;
+ }
+
+ /**
+ * Creates a vertex array from the height data in this Geomap
+ *
+ * The scale argument specifies the scale to use for the vertex buffer.
+ * For example, if scale is 10,1,10, then the greatest X value is getWidth()*10
+ *
+ * @param store A preallocated FloatBuffer where to store the data (optional), size must be >= getWidth()*getHeight()*3
+ * @param scale Created vertexes are scaled by this vector
+ *
+ * @returns store, or a new FloatBuffer if store is null
+ *
+ * @throws NullPointerException If isLoaded() is false
+ */
+ public FloatBuffer writeVertexArray(FloatBuffer store, Vector3f scale, boolean center) {
+
+ if (store!=null){
+ if (store.remaining() < width*height*3)
+ throw new BufferUnderflowException();
+ }else{
+ store = BufferUtils.createFloatBuffer(width*height*3);
+ }
+
+ assert hdata.length == height*width;
+
+ Vector3f offset = new Vector3f(-getWidth() * scale.x * 0.5f,
+ 0,
+ -getWidth() * scale.z * 0.5f);
+ if (!center)
+ offset.zero();
+
+ int i = 0;
+ for (int z = 0; z < height; z++){
+ for (int x = 0; x < width; x++){
+ store.put( (float)x*scale.x + offset.x );
+ store.put( (float)hdata[i++]*scale.y );
+ store.put( (float)z*scale.z + offset.z );
+ }
+ }
+
+ return store;
+ }
+
+ public Vector2f getUV(int x, int y, Vector2f store){
+ store.set( (float)x / (float)getWidth(),
+ (float)y / (float)getHeight() );
+ return store;
+ }
+
+ public Vector2f getUV(int i, Vector2f store){
+ return store;
+ }
+
+ public FloatBuffer writeTexCoordArray(FloatBuffer store, Vector2f offset, Vector2f scale){
+ if (store!=null){
+ if (store.remaining() < getWidth()*getHeight()*2)
+ throw new BufferUnderflowException();
+ }else{
+ store = BufferUtils.createFloatBuffer(getWidth()*getHeight()*2);
+ }
+
+ if (offset == null)
+ offset = new Vector2f();
+
+ Vector2f tcStore = new Vector2f();
+ for (int y = 0; y < getHeight(); y++){
+ for (int x = 0; x < getWidth(); x++){
+ getUV(x,y,tcStore);
+ store.put( offset.x + tcStore.x * scale.x );
+ store.put( offset.y + tcStore.y * scale.y );
+ }
+
+ }
+
+ return store;
+ }
+
+ public IntBuffer writeIndexArray(IntBuffer store){
+ int faceN = (getWidth()-1)*(getHeight()-1)*2;
+
+ if (store!=null){
+ if (store.remaining() < faceN*3)
+ throw new BufferUnderflowException();
+ }else{
+ store = BufferUtils.createIntBuffer(faceN*3);
+ }
+
+ int i = 0;
+ for (int z = 0; z < getHeight()-1; z++){
+ for (int x = 0; x < getWidth()-1; x++){
+ store.put(i).put(i+getWidth()).put(i+getWidth()+1);
+ store.put(i+getWidth()+1).put(i+1).put(i);
+ i++;
+
+ // TODO: There's probably a better way to do this..
+ if (x==getWidth()-2) i++;
+ }
+ }
+ store.flip();
+
+ return store;
+ }
+
+ public Mesh createMesh(Vector3f scale, Vector2f tcScale, boolean center){
+ FloatBuffer pb = writeVertexArray(null, scale, center);
+ FloatBuffer tb = writeTexCoordArray(null, Vector2f.ZERO, tcScale);
+ FloatBuffer nb = writeNormalArray(null, scale);
+ IntBuffer ib = writeIndexArray(null);
+ Mesh m = new Mesh();
+ m.setBuffer(Type.Position, 3, pb);
+ m.setBuffer(Type.Normal, 3, nb);
+ m.setBuffer(Type.TexCoord, 2, tb);
+ m.setBuffer(Type.Index, 3, ib);
+ m.setStatic();
+ m.updateBound();
+ return m;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(hdata, "hdataarray", null);
+ oc.write(width, "width", 0);
+ oc.write(height, "height", 0);
+ oc.write(maxval, "maxval", 0);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule ic = im.getCapsule(this);
+ hdata = ic.readFloatArray("hdataarray", null);
+ if (hdata == null) {
+ FloatBuffer buf = ic.readFloatBuffer("hdata", null);
+ if (buf != null) {
+ hdata = new float[buf.limit()];
+ buf.get(hdata);
+ }
+ }
+ width = ic.readInt("width", 0);
+ height = ic.readInt("height", 0);
+ maxval = ic.readInt("maxval", 0);
+ }
+
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/ProgressMonitor.java b/engine/src/terrain/com/jme3/terrain/ProgressMonitor.java
new file mode 100644
index 0000000..4664231
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/ProgressMonitor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain;
+
+/**
+ * Monitor the progress of an expensive terrain operation.
+ *
+ * Monitors are passed into the expensive operations, and those operations
+ * call the incrementProgress method whenever they determine that progress
+ * has changed. It is up to the monitor to determine if the increment is
+ * percentage or a unit of another measure, but anything calling it should
+ * use the setMonitorMax() method and make sure incrementProgress() match up
+ * in terms of units.
+ *
+ * @author Brent Owens
+ */
+public interface ProgressMonitor {
+
+ /**
+ * Increment the progress by a unit.
+ */
+ public void incrementProgress(float increment);
+
+ /**
+ * The max value that when reached, the progress is at 100%.
+ */
+ public void setMonitorMax(float max);
+
+ /**
+ * The max value of the progress. When incrementProgress()
+ * reaches this value, progress is complete
+ */
+ public float getMonitorMax();
+
+ /**
+ * The progress has completed
+ */
+ public void progressComplete();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/Terrain.java b/engine/src/terrain/com/jme3/terrain/Terrain.java
new file mode 100644
index 0000000..6f3872e
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/Terrain.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain;
+
+import com.jme3.material.Material;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
+import java.util.List;
+
+/**
+ * Terrain can be one or many meshes comprising of a, probably large, piece of land.
+ * Terrain is Y-up in the grid axis, meaning gravity acts in the -Y direction.
+ * Level of Detail (LOD) is supported and expected as terrains can get very large. LOD can
+ * also be disabled if you so desire, however some terrain implementations can choose to ignore
+ * useLOD(boolean).
+ * Terrain implementations should extend Node, or at least Spatial.
+ *
+ * @author bowens
+ */
+public interface Terrain {
+
+ /**
+ * Get the real-world height of the terrain at the specified X-Z coorindate.
+ * @param xz the X-Z world coordinate
+ * @return the height at the given point
+ */
+ public float getHeight(Vector2f xz);
+
+ /**
+ * Get the normal vector for the surface of the terrain at the specified
+ * X-Z coordinate. This normal vector can be a close approximation. It does not
+ * take into account any normal maps on the material.
+ * @param xz the X-Z world coordinate
+ * @return the normal vector at the given point
+ */
+ public Vector3f getNormal(Vector2f xz);
+
+ /**
+ * Get the heightmap height at the specified X-Z coordinate. This does not
+ * count scaling and snaps the XZ coordinate to the nearest (rounded) heightmap grid point.
+ * @param xz world coordinate
+ * @return the height, unscaled and uninterpolated
+ */
+ public float getHeightmapHeight(Vector2f xz);
+
+ /**
+ * Set the height at the specified X-Z coordinate.
+ * To set the height of the terrain and see it, you will have
+ * to unlock the terrain meshes by calling terrain.setLocked(false) before
+ * you call setHeight().
+ * @param xzCoordinate coordinate to set the height
+ * @param height that will be set at the coordinate
+ */
+ public void setHeight(Vector2f xzCoordinate, float height);
+
+ /**
+ * Set the height at many points. The two lists must be the same size.
+ * Each xz coordinate entry matches to a height entry, 1 for 1. So the
+ * first coordinate matches to the first height value, the last to the
+ * last etc.
+ * @param xz a list of coordinates where the hight will be set
+ * @param height the heights that match the xz coordinates
+ */
+ public void setHeight(List<Vector2f> xz, List<Float> height);
+
+ /**
+ * Raise/lower the height in one call (instead of getHeight then setHeight).
+ * @param xzCoordinate world coordinate to adjust the terrain height
+ * @param delta +- value to adjust the height by
+ */
+ public void adjustHeight(Vector2f xzCoordinate, float delta);
+
+ /**
+ * Raise/lower the height at many points. The two lists must be the same size.
+ * Each xz coordinate entry matches to a height entry, 1 for 1. So the
+ * first coordinate matches to the first height value, the last to the
+ * last etc.
+ * @param xz a list of coordinates where the hight will be adjusted
+ * @param height +- value to adjust the height by, that matches the xz coordinates
+ */
+ public void adjustHeight(List<Vector2f> xz, List<Float> height);
+
+ /**
+ * Get the heightmap of the entire terrain.
+ * This can return null if that terrain object does not store the height data.
+ * Infinite or "paged" terrains will not be able to support this, so use with caution.
+ */
+ public float[] getHeightMap();
+
+ /**
+ * This is calculated by the specific LOD algorithm.
+ * A value of one means that the terrain is showing full detail.
+ * The higher the value, the more the terrain has been generalized
+ * and the less detailed it will be.
+ */
+ public int getMaxLod();
+
+ /**
+ * Called by an LodControl.
+ * Calculates the level of detail of the terrain and adjusts its geometry.
+ * This is where the Terrain's LOD algorithm will change the detail of
+ * the terrain based on how far away this position is from the particular
+ * terrain patch.
+ * @param location the Camera's location. A list of one camera location is normal
+ * if you just have one camera in your scene.
+ */
+ public void update(List<Vector3f> location, LodCalculator lodCalculator);
+
+ /**
+ * Lock or unlock the meshes of this terrain.
+ * Locked meshes are un-editable but have better performance.
+ * This should call the underlying getMesh().setStatic()/setDynamic() methods.
+ * @param locked or unlocked
+ */
+ public void setLocked(boolean locked);
+
+ /**
+ * Pre-calculate entropy values.
+ * Some terrain systems support entropy calculations to determine LOD
+ * changes. Often these entropy calculations are expensive and can be
+ * cached ahead of time. Use this method to do that.
+ */
+ public void generateEntropy(ProgressMonitor monitor);
+
+ /**
+ * Returns the material that this terrain uses.
+ * If it uses many materials, just return the one you think is best.
+ * For TerrainQuads this is sufficient. For TerrainGrid you want to call
+ * getMaterial(Vector3f) instead.
+ */
+ public Material getMaterial();
+
+ /**
+ * Returns the material that this terrain uses.
+ * Terrain can have different materials in different locations.
+ * In general, the TerrainQuad will only have one material. But
+ * TerrainGrid will have a different material per tile.
+ *
+ * It could be possible to pass in null for the location, some Terrain
+ * implementations might just have the one material and not care where
+ * you are looking. So implementations must handle null being supplied.
+ *
+ * @param worldLocation the location, in world coordinates, of where
+ * we are interested in the underlying texture.
+ */
+ public Material getMaterial(Vector3f worldLocation);
+
+ /**
+ * Used for painting to get the number of vertices along the edge of the
+ * terrain.
+ * This is an un-scaled size, and should represent the vertex count (ie. the
+ * texture coord count) along an edge of a square terrain.
+ *
+ * In the standard TerrainQuad default implementation, this will return
+ * the "totalSize" of the terrain (512 or so).
+ */
+ public int getTerrainSize();
+
+ /**
+ * Get the scale of the texture coordinates. Normally if the texture is
+ * laid on the terrain and not scaled so that the texture does not repeat,
+ * then each texture coordinate (on a vertex) will be 1/(terrain size).
+ * That is: the coverage between each consecutive texture coordinate will
+ * be a percentage of the total terrain size.
+ * So if the terrain is 512 vertexes wide, then each texture coord will cover
+ * 1/512 (or 0.00195) percent of the texture.
+ * This is used for converting between tri-planar texture scales and regular
+ * texture scales.
+ *
+ * not needed
+ */
+ //public float getTextureCoordinateScale();
+
+ /**
+ *
+ *
+ */
+ public int getNumMajorSubdivisions();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/LODGeomap.java b/engine/src/terrain/com/jme3/terrain/geomipmap/LODGeomap.java
new file mode 100644
index 0000000..3944b98
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/LODGeomap.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.math.FastMath;
+import com.jme3.math.Triangle;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Mesh.Mode;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.terrain.GeoMap;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.TempVars;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+/**
+ * Produces the mesh for the TerrainPatch.
+ * This LOD algorithm generates a single triangle strip by first building the center of the
+ * mesh, minus one outer edge around it. Then it builds the edges in counter-clockwise order,
+ * starting at the bottom right and working up, then left across the top, then down across the
+ * left, then right across the bottom.
+ * It needs to know what its neighbour's LOD's are so it can stitch the edges.
+ * It creates degenerate polygons in order to keep the winding order of the polygons and to move
+ * the strip to a new position while still maintaining the continuity of the overall mesh. These
+ * degenerates are removed quickly by the video card.
+ *
+ * @author Brent Owens
+ */
+public class LODGeomap extends GeoMap {
+
+ public LODGeomap() {
+ }
+
+ @Deprecated
+ public LODGeomap(int size, FloatBuffer heightMap) {
+ super(heightMap, size, size, 1);
+ }
+
+ public LODGeomap(int size, float[] heightMap) {
+ super(heightMap, size, size, 1);
+ }
+
+ public Mesh createMesh(Vector3f scale, Vector2f tcScale, Vector2f tcOffset, float offsetAmount, int totalSize, boolean center) {
+ return this.createMesh(scale, tcScale, tcOffset, offsetAmount, totalSize, center, 1, false, false, false, false);
+ }
+
+ public Mesh createMesh(Vector3f scale, Vector2f tcScale, Vector2f tcOffset, float offsetAmount, int totalSize, boolean center, int lod, boolean rightLod, boolean topLod, boolean leftLod, boolean bottomLod) {
+ FloatBuffer pb = writeVertexArray(null, scale, center);
+ FloatBuffer texb = writeTexCoordArray(null, tcOffset, tcScale, offsetAmount, totalSize);
+ FloatBuffer nb = writeNormalArray(null, scale);
+ IntBuffer ib = writeIndexArrayLodDiff(null, lod, rightLod, topLod, leftLod, bottomLod);
+ FloatBuffer bb = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
+ FloatBuffer tanb = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
+ writeTangentArray(tanb, bb, texb, scale);
+ Mesh m = new Mesh();
+ m.setMode(Mode.TriangleStrip);
+ m.setBuffer(Type.Position, 3, pb);
+ m.setBuffer(Type.Normal, 3, nb);
+ m.setBuffer(Type.Tangent, 3, tanb);
+ m.setBuffer(Type.Binormal, 3, bb);
+ m.setBuffer(Type.TexCoord, 2, texb);
+ m.setBuffer(Type.Index, 3, ib);
+ m.setStatic();
+ m.updateBound();
+ return m;
+ }
+
+ public FloatBuffer writeTexCoordArray(FloatBuffer store, Vector2f offset, Vector2f scale, float offsetAmount, int totalSize) {
+ if (store != null) {
+ if (store.remaining() < getWidth() * getHeight() * 2) {
+ throw new BufferUnderflowException();
+ }
+ } else {
+ store = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 2);
+ }
+
+ if (offset == null) {
+ offset = new Vector2f();
+ }
+
+ Vector2f tcStore = new Vector2f();
+
+ // work from bottom of heightmap up, so we don't flip the coords
+ for (int y = getHeight() - 1; y >= 0; y--) {
+ for (int x = 0; x < getWidth(); x++) {
+ getUV(x, y, tcStore, offset, offsetAmount, totalSize);
+ float tx = tcStore.x * scale.x;
+ float ty = tcStore.y * scale.y;
+ store.put(tx);
+ store.put(ty);
+ }
+ }
+
+ return store;
+ }
+
+ public Vector2f getUV(int x, int y, Vector2f store, Vector2f offset, float offsetAmount, int totalSize) {
+ float offsetX = offset.x + (offsetAmount * 1.0f);
+ float offsetY = -offset.y + (offsetAmount * 1.0f);//note the -, we flip the tex coords
+
+ store.set((((float) x) + offsetX) / (float) (totalSize - 1), // calculates percentage of texture here
+ (((float) y) + offsetY) / (float) (totalSize - 1));
+ return store;
+ }
+
+ /**
+ * Create the LOD index array that will seam its edges with its neighbour's LOD.
+ * This is a scary method!!! It will break your mind.
+ *
+ * @param store to store the index buffer
+ * @param lod level of detail of the mesh
+ * @param rightLod LOD of the right neighbour
+ * @param topLod LOD of the top neighbour
+ * @param leftLod LOD of the left neighbour
+ * @param bottomLod LOD of the bottom neighbour
+ * @return the LOD-ified index buffer
+ */
+ public IntBuffer writeIndexArrayLodDiff(IntBuffer store, int lod, boolean rightLod, boolean topLod, boolean leftLod, boolean bottomLod) {
+
+ IntBuffer buffer2 = store;
+ int numIndexes = calculateNumIndexesLodDiff(lod);
+ if (store == null) {
+ buffer2 = BufferUtils.createIntBuffer(numIndexes);
+ }
+ VerboseIntBuffer buffer = new VerboseIntBuffer(buffer2);
+
+
+ // generate center squares minus the edges
+ //System.out.println("for (x="+lod+"; x<"+(getWidth()-(2*lod))+"; x+="+lod+")");
+ //System.out.println(" for (z="+lod+"; z<"+(getWidth()-(1*lod))+"; z+="+lod+")");
+ for (int r = lod; r < getWidth() - (2 * lod); r += lod) { // row
+ int rowIdx = r * getWidth();
+ int nextRowIdx = (r + 1 * lod) * getWidth();
+ for (int c = lod; c < getWidth() - (1 * lod); c += lod) { // column
+ int idx = rowIdx + c;
+ buffer.put(idx);
+ idx = nextRowIdx + c;
+ buffer.put(idx);
+ }
+
+ // add degenerate triangles
+ if (r < getWidth() - (3 * lod)) {
+ int idx = nextRowIdx + getWidth() - (1 * lod) - 1;
+ buffer.put(idx);
+ idx = nextRowIdx + (1 * lod); // inset by 1
+ buffer.put(idx);
+ //System.out.println("");
+ }
+ }
+ //System.out.println("\nright:");
+
+ //int runningBufferCount = buffer.getCount();
+ //System.out.println("buffer start: "+runningBufferCount);
+
+
+ // right
+ int br = getWidth() * (getWidth() - lod) - 1 - lod;
+ buffer.put(br); // bottom right -1
+ int corner = getWidth() * getWidth() - 1;
+ buffer.put(corner); // bottom right corner
+ if (rightLod) { // if lower LOD
+ for (int row = getWidth() - lod; row >= 1 + lod; row -= 2 * lod) {
+ int idx = (row) * getWidth() - 1 - lod;
+ buffer.put(idx);
+ idx = (row - lod) * getWidth() - 1;
+ buffer.put(idx);
+ if (row > lod + 1) { //if not the last one
+ idx = (row - lod) * getWidth() - 1 - lod;
+ buffer.put(idx);
+ idx = (row - lod) * getWidth() - 1;
+ buffer.put(idx);
+ } else {
+ }
+ }
+ } else {
+ buffer.put(corner);//br+1);//degenerate to flip winding order
+ for (int row = getWidth() - lod; row > lod; row -= lod) {
+ int idx = row * getWidth() - 1; // mult to get row
+ buffer.put(idx);
+ buffer.put(idx - lod);
+ }
+
+ }
+
+ buffer.put(getWidth() - 1);
+
+
+ //System.out.println("\nbuffer right: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+
+ //System.out.println("\ntop:");
+
+ // top (the order gets reversed here so the diagonals line up)
+ if (topLod) { // if lower LOD
+ if (rightLod) {
+ buffer.put(getWidth() - 1);
+ }
+ for (int col = getWidth() - 1; col >= lod; col -= 2 * lod) {
+ int idx = (lod * getWidth()) + col - lod; // next row
+ buffer.put(idx);
+ idx = col - 2 * lod;
+ buffer.put(idx);
+ if (col > lod * 2) { //if not the last one
+ idx = (lod * getWidth()) + col - 2 * lod;
+ buffer.put(idx);
+ idx = col - 2 * lod;
+ buffer.put(idx);
+ } else {
+ }
+ }
+ } else {
+ if (rightLod) {
+ buffer.put(getWidth() - 1);
+ }
+ for (int col = getWidth() - 1 - lod; col > 0; col -= lod) {
+ int idx = col + (lod * getWidth());
+ buffer.put(idx);
+ idx = col;
+ buffer.put(idx);
+ }
+ buffer.put(0);
+ }
+ buffer.put(0);
+
+ //System.out.println("\nbuffer top: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //System.out.println("\nleft:");
+
+ // left
+ if (leftLod) { // if lower LOD
+ if (topLod) {
+ buffer.put(0);
+ }
+ for (int row = 0; row < getWidth() - lod; row += 2 * lod) {
+ int idx = (row + lod) * getWidth() + lod;
+ buffer.put(idx);
+ idx = (row + 2 * lod) * getWidth();
+ buffer.put(idx);
+ if (row < getWidth() - lod - 2 - 1) { //if not the last one
+ idx = (row + 2 * lod) * getWidth() + lod;
+ buffer.put(idx);
+ idx = (row + 2 * lod) * getWidth();
+ buffer.put(idx);
+ } else {
+ }
+ }
+ } else {
+ if (!topLod) {
+ buffer.put(0);
+ }
+ //buffer.put(getWidth()+1); // degenerate
+ //buffer.put(0); // degenerate winding-flip
+ for (int row = lod; row < getWidth() - lod; row += lod) {
+ int idx = row * getWidth();
+ buffer.put(idx);
+ idx = row * getWidth() + lod;
+ buffer.put(idx);
+ }
+
+ }
+ buffer.put(getWidth() * (getWidth() - 1));
+
+
+ //System.out.println("\nbuffer left: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //if (true) return buffer.delegate;
+ //System.out.println("\nbottom");
+
+ // bottom
+ if (bottomLod) { // if lower LOD
+ if (leftLod) {
+ buffer.put(getWidth() * (getWidth() - 1));
+ }
+ // there was a slight bug here when really high LOD near maxLod
+ // far right has extra index one row up and all the way to the right, need to skip last index entered
+ // seemed to be fixed by making "getWidth()-1-2-lod" this: "getWidth()-1-2*lod", which seems more correct
+ for (int col = 0; col < getWidth() - lod; col += 2 * lod) {
+ int idx = getWidth() * (getWidth() - 1 - lod) + col + lod;
+ buffer.put(idx);
+ idx = getWidth() * (getWidth() - 1) + col + 2 * lod;
+ buffer.put(idx);
+ if (col < getWidth() - 1 - 2 * lod) { //if not the last one
+ idx = getWidth() * (getWidth() - 1 - lod) + col + 2 * lod;
+ buffer.put(idx);
+ idx = getWidth() * (getWidth() - 1) + col + 2 * lod;
+ buffer.put(idx);
+ } else {
+ }
+ }
+ } else {
+ if (leftLod) {
+ buffer.put(getWidth() * (getWidth() - 1));
+ }
+ for (int col = lod; col < getWidth() - lod; col += lod) {
+ int idx = getWidth() * (getWidth() - 1 - lod) + col; // up
+ buffer.put(idx);
+ idx = getWidth() * (getWidth() - 1) + col; // down
+ buffer.put(idx);
+ }
+ //buffer.put(getWidth()*getWidth()-1-lod); // <-- THIS caused holes at the end!
+ }
+
+ buffer.put(getWidth() * getWidth() - 1);
+
+ //System.out.println("\nbuffer bottom: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //System.out.println("\nBuffer size: "+buffer.getCount());
+
+ // fill in the rest of the buffer with degenerates, there should only be a couple
+ for (int i = buffer.getCount(); i < numIndexes; i++) {
+ buffer.put(getWidth() * getWidth() - 1);
+ }
+
+ return buffer.delegate;
+ }
+
+ public IntBuffer writeIndexArrayLodVariable(IntBuffer store, int lod, int rightLod, int topLod, int leftLod, int bottomLod) {
+
+ IntBuffer buffer2 = store;
+ int numIndexes = calculateNumIndexesLodDiff(lod);
+ if (store == null) {
+ buffer2 = BufferUtils.createIntBuffer(numIndexes);
+ }
+ VerboseIntBuffer buffer = new VerboseIntBuffer(buffer2);
+
+
+ // generate center squares minus the edges
+ //System.out.println("for (x="+lod+"; x<"+(getWidth()-(2*lod))+"; x+="+lod+")");
+ //System.out.println(" for (z="+lod+"; z<"+(getWidth()-(1*lod))+"; z+="+lod+")");
+ for (int r = lod; r < getWidth() - (2 * lod); r += lod) { // row
+ int rowIdx = r * getWidth();
+ int nextRowIdx = (r + 1 * lod) * getWidth();
+ for (int c = lod; c < getWidth() - (1 * lod); c += lod) { // column
+ int idx = rowIdx + c;
+ buffer.put(idx);
+ idx = nextRowIdx + c;
+ buffer.put(idx);
+ }
+
+ // add degenerate triangles
+ if (r < getWidth() - (3 * lod)) {
+ int idx = nextRowIdx + getWidth() - (1 * lod) - 1;
+ buffer.put(idx);
+ idx = nextRowIdx + (1 * lod); // inset by 1
+ buffer.put(idx);
+ //System.out.println("");
+ }
+ }
+ //System.out.println("\nright:");
+
+ //int runningBufferCount = buffer.getCount();
+ //System.out.println("buffer start: "+runningBufferCount);
+
+
+ // right
+ int br = getWidth() * (getWidth() - lod) - 1 - lod;
+ buffer.put(br); // bottom right -1
+ int corner = getWidth() * getWidth() - 1;
+ buffer.put(corner); // bottom right corner
+ if (rightLod > lod) { // if lower LOD
+ int idx = corner;
+ int it = (getWidth() - 1) / rightLod; // iterations
+ int lodDiff = rightLod / lod;
+ for (int i = it; i > 0; i--) { // for each lod level of the neighbour
+ idx = getWidth() * (i * rightLod + 1) - 1;
+ for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
+ int idxB = idx - (getWidth() * (j * lod)) - lod;
+
+ if (j == lodDiff && i == 1) {// the last one
+ buffer.put(getWidth() - 1);
+ } else if (j == lodDiff) {
+ buffer.put(idxB);
+ buffer.put(idxB + lod);
+ } else {
+ buffer.put(idxB);
+ buffer.put(idx);
+ }
+ }
+ }
+ // reset winding order
+ buffer.put(getWidth() * (lod + 1) - lod - 1); // top-right +1row
+ buffer.put(getWidth() - 1);// top-right
+
+ } else {
+ buffer.put(corner);//br+1);//degenerate to flip winding order
+ for (int row = getWidth() - lod; row > lod; row -= lod) {
+ int idx = row * getWidth() - 1; // mult to get row
+ buffer.put(idx);
+ buffer.put(idx - lod);
+ }
+ buffer.put(getWidth() - 1);
+ }
+
+
+ //System.out.println("\nbuffer right: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+
+ //System.out.println("\ntop:");
+
+ // top (the order gets reversed here so the diagonals line up)
+ if (topLod > lod) { // if lower LOD
+ if (rightLod > lod) {
+ // need to flip winding order
+ buffer.put(getWidth() - 1);
+ buffer.put(getWidth() * lod - 1);
+ buffer.put(getWidth() - 1);
+ }
+ int idx = getWidth() - 1;
+ int it = (getWidth() - 1) / topLod; // iterations
+ int lodDiff = topLod / lod;
+ for (int i = it; i > 0; i--) { // for each lod level of the neighbour
+ idx = (i * topLod);
+ for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
+ int idxB = lod * getWidth() + (i * topLod) - (j * lod);
+
+ if (j == lodDiff && i == 1) {// the last one
+ buffer.put(0);
+ } else if (j == lodDiff) {
+ buffer.put(idxB);
+ buffer.put(idx - topLod);
+ } else {
+ buffer.put(idxB);
+ buffer.put(idx);
+ }
+ }
+ }
+ } else {
+ if (rightLod > lod) {
+ buffer.put(getWidth() - 1);
+ }
+ for (int col = getWidth() - 1 - lod; col > 0; col -= lod) {
+ int idx = col + (lod * getWidth());
+ buffer.put(idx);
+ idx = col;
+ buffer.put(idx);
+ }
+ buffer.put(0);
+ }
+ buffer.put(0);
+
+ //System.out.println("\nbuffer top: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //System.out.println("\nleft:");
+
+ // left
+ if (leftLod > lod) { // if lower LOD
+
+ int idx = 0;
+ int it = (getWidth() - 1) / leftLod; // iterations
+ int lodDiff = leftLod / lod;
+ for (int i = 0; i < it; i++) { // for each lod level of the neighbour
+ idx = getWidth() * (i * leftLod);
+ for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
+ int idxB = idx + (getWidth() * (j * lod)) + lod;
+
+ if (j == lodDiff && i == it - 1) {// the last one
+ buffer.put(getWidth() * getWidth() - getWidth());
+ } else if (j == lodDiff) {
+ buffer.put(idxB);
+ buffer.put(idxB - lod);
+ } else {
+ buffer.put(idxB);
+ buffer.put(idx);
+ }
+ }
+ }
+
+ } else {
+ buffer.put(0);
+ buffer.put(getWidth() * lod + lod);
+ buffer.put(0);
+ for (int row = lod; row < getWidth() - lod; row += lod) {
+ int idx = row * getWidth();
+ buffer.put(idx);
+ idx = row * getWidth() + lod;
+ buffer.put(idx);
+ }
+ buffer.put(getWidth() * (getWidth() - 1));
+ }
+ //buffer.put(getWidth()*(getWidth()-1));
+
+
+ //System.out.println("\nbuffer left: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //if (true) return buffer.delegate;
+ //System.out.println("\nbottom");
+
+ // bottom
+ if (bottomLod > lod) { // if lower LOD
+ if (leftLod > lod) {
+ buffer.put(getWidth() * (getWidth() - 1));
+ buffer.put(getWidth() * (getWidth() - lod));
+ buffer.put(getWidth() * (getWidth() - 1));
+ }
+
+ int idx = getWidth() * getWidth() - getWidth();
+ int it = (getWidth() - 1) / bottomLod; // iterations
+ int lodDiff = bottomLod / lod;
+ for (int i = 0; i < it; i++) { // for each lod level of the neighbour
+ idx = getWidth() * getWidth() - getWidth() + (i * bottomLod);
+ for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
+ int idxB = idx - (getWidth() * lod) + j * lod;
+
+ if (j == lodDiff && i == it - 1) {// the last one
+ buffer.put(getWidth() * getWidth() - 1);
+ } else if (j == lodDiff) {
+ buffer.put(idxB);
+ buffer.put(idx + bottomLod);
+ } else {
+ buffer.put(idxB);
+ buffer.put(idx);
+ }
+ }
+ }
+ } else {
+ if (leftLod > lod) {
+ buffer.put(getWidth() * (getWidth() - 1));
+ buffer.put(getWidth() * getWidth() - (getWidth() * lod) + lod);
+ buffer.put(getWidth() * (getWidth() - 1));
+ }
+ for (int col = lod; col < getWidth() - lod; col += lod) {
+ int idx = getWidth() * (getWidth() - 1 - lod) + col; // up
+ buffer.put(idx);
+ idx = getWidth() * (getWidth() - 1) + col; // down
+ buffer.put(idx);
+ }
+ //buffer.put(getWidth()*getWidth()-1-lod); // <-- THIS caused holes at the end!
+ }
+
+ buffer.put(getWidth() * getWidth() - 1);
+
+ //System.out.println("\nbuffer bottom: "+(buffer.getCount()-runningBufferCount));
+ //runningBufferCount = buffer.getCount();
+
+ //System.out.println("\nBuffer size: "+buffer.getCount());
+
+ // fill in the rest of the buffer with degenerates, there should only be a couple
+ for (int i = buffer.getCount(); i < numIndexes; i++) {
+ buffer.put(getWidth() * getWidth() - 1);
+ }
+
+ return buffer.delegate;
+ }
+
+
+ /*private int calculateNumIndexesNormal(int lod) {
+ int length = getWidth()-1;
+ int num = ((length/lod)+1)*((length/lod)+1)*2;
+ System.out.println("num: "+num);
+ num -= 2*((length/lod)+1);
+ System.out.println("num2: "+num);
+ // now get the degenerate indexes that exist between strip rows
+ num += 2*(((length/lod)+1)-2); // every row except the first and last
+ System.out.println("Index buffer size: "+num);
+ return num;
+ }*/
+ /**
+ * calculate how many indexes there will be.
+ * This isn't that precise and there might be a couple extra.
+ */
+ private int calculateNumIndexesLodDiff(int lod) {
+ if (lod == 0) {
+ lod = 1;
+ }
+ int length = getWidth() - 1; // make it even for lod calc
+ int side = (length / lod) + 1 - (2);
+ //System.out.println("side: "+side);
+ int num = side * side * 2;
+ //System.out.println("num: "+num);
+ num -= 2 * side; // remove one first row and one last row (they are only hit once each)
+ //System.out.println("num2: "+num);
+ // now get the degenerate indexes that exist between strip rows
+ int degenerates = 2 * (side - (2)); // every row except the first and last
+ num += degenerates;
+ //System.out.println("degenerates: "+degenerates);
+
+ //System.out.println("center, before edges: "+num);
+
+ num += (getWidth() / lod) * 2 * 4;
+ num++;
+
+ num += 10;// TODO remove me: extra
+ //System.out.println("Index buffer size: "+num);
+ return num;
+ }
+
+ public FloatBuffer[] writeTangentArray(FloatBuffer tangentStore, FloatBuffer binormalStore, FloatBuffer textureBuffer, Vector3f scale) {
+ if (!isLoaded()) {
+ throw new NullPointerException();
+ }
+
+ if (tangentStore != null) {
+ if (tangentStore.remaining() < getWidth() * getHeight() * 3) {
+ throw new BufferUnderflowException();
+ }
+ } else {
+ tangentStore = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
+ }
+ tangentStore.rewind();
+
+ if (binormalStore != null) {
+ if (binormalStore.remaining() < getWidth() * getHeight() * 3) {
+ throw new BufferUnderflowException();
+ }
+ } else {
+ binormalStore = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
+ }
+ binormalStore.rewind();
+
+ Vector3f tangent = new Vector3f();
+ Vector3f binormal = new Vector3f();
+ Vector3f v1 = new Vector3f();
+ Vector3f v2 = new Vector3f();
+ Vector3f v3 = new Vector3f();
+ Vector2f t1 = new Vector2f();
+ Vector2f t2 = new Vector2f();
+ Vector2f t3 = new Vector2f();
+
+ //scale = Vector3f.UNIT_XYZ;
+
+ for (int r = 0; r < getHeight(); r++) {
+ for (int c = 0; c < getWidth(); c++) {
+
+ int texIdx = ((getHeight() - 1 - r) * getWidth() + c) * 2; // pull from the end
+ int texIdxAbove = ((getHeight() - 1 - (r - 1)) * getWidth() + c) * 2; // pull from the end
+ int texIdxNext = ((getHeight() - 1 - (r + 1)) * getWidth() + c) * 2; // pull from the end
+
+ v1.set(c, getValue(c, r), r);
+ t1.set(textureBuffer.get(texIdx), textureBuffer.get(texIdx + 1));
+
+ // below
+ if (r == getHeight()-1) { // last row
+ v3.set(c, getValue(c, r), r + 1);
+ float u = textureBuffer.get(texIdx) - textureBuffer.get(texIdxAbove);
+ u += textureBuffer.get(texIdx);
+ float v = textureBuffer.get(texIdx + 1) - textureBuffer.get(texIdxAbove + 1);
+ v += textureBuffer.get(texIdx + 1);
+ t3.set(u, v);
+ } else {
+ v3.set(c, getValue(c, r + 1), r + 1);
+ t3.set(textureBuffer.get(texIdxNext), textureBuffer.get(texIdxNext + 1));
+ }
+
+ //right
+ if (c == getWidth()-1) { // last column
+ v2.set(c + 1, getValue(c, r), r);
+ float u = textureBuffer.get(texIdx) - textureBuffer.get(texIdx - 2);
+ u += textureBuffer.get(texIdx);
+ float v = textureBuffer.get(texIdx + 1) - textureBuffer.get(texIdx - 1);
+ v += textureBuffer.get(texIdx - 1);
+ t2.set(u, v);
+ } else {
+ v2.set(c + 1, getValue(c + 1, r), r); // one to the right
+ t2.set(textureBuffer.get(texIdx + 2), textureBuffer.get(texIdx + 3));
+ }
+
+ calculateTangent(new Vector3f[]{v1.mult(scale), v2.mult(scale), v3.mult(scale)}, new Vector2f[]{t1, t2, t3}, tangent, binormal);
+ BufferUtils.setInBuffer(tangent, tangentStore, (r * getWidth() + c)); // save the tangent
+ BufferUtils.setInBuffer(binormal, binormalStore, (r * getWidth() + c)); // save the binormal
+ }
+ }
+
+ return new FloatBuffer[]{tangentStore, binormalStore};
+ }
+
+ /**
+ *
+ * @param v Takes 3 vertices: root, right, bottom
+ * @param t Takes 3 tex coords: root, right, bottom
+ * @param tangent that will store the result
+ * @return the tangent store
+ */
+ public static Vector3f calculateTangent(Vector3f[] v, Vector2f[] t, Vector3f tangent, Vector3f binormal) {
+ Vector3f edge1 = new Vector3f(); // y=0
+ Vector3f edge2 = new Vector3f(); // x=0
+ Vector2f edge1uv = new Vector2f(); // y=0
+ Vector2f edge2uv = new Vector2f(); // x=0
+
+ t[2].subtract(t[0], edge2uv);
+ t[1].subtract(t[0], edge1uv);
+
+ float det = edge1uv.x * edge2uv.y;// - edge1uv.y*edge2uv.x; = 0
+
+ boolean normalize = true;
+ if (Math.abs(det) < 0.0000001f) {
+ det = 1;
+ normalize = true;
+ }
+
+ v[1].subtract(v[0], edge1);
+ v[2].subtract(v[0], edge2);
+
+ tangent.set(edge1);
+ tangent.normalizeLocal();
+ binormal.set(edge2);
+ binormal.normalizeLocal();
+
+ float factor = 1 / det;
+ tangent.x = (edge2uv.y * edge1.x) * factor;
+ tangent.y = 0;
+ tangent.z = (edge2uv.y * edge1.z) * factor;
+ if (normalize) {
+ tangent.normalizeLocal();
+ }
+
+ binormal.x = 0;
+ binormal.y = (edge1uv.x * edge2.y) * factor;
+ binormal.z = (edge1uv.x * edge2.z) * factor;
+ if (normalize) {
+ binormal.normalizeLocal();
+ }
+
+ return tangent;
+ }
+
+ @Override
+ public FloatBuffer writeNormalArray(FloatBuffer store, Vector3f scale) {
+ if (!isLoaded()) {
+ throw new NullPointerException();
+ }
+
+ if (store != null) {
+ if (store.remaining() < getWidth() * getHeight() * 3) {
+ throw new BufferUnderflowException();
+ }
+ } else {
+ store = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
+ }
+ store.rewind();
+
+ TempVars vars = TempVars.get();
+
+ Vector3f rootPoint = vars.vect1;
+ Vector3f rightPoint = vars.vect2;
+ Vector3f leftPoint = vars.vect3;
+ Vector3f topPoint = vars.vect4;
+ Vector3f bottomPoint = vars.vect5;
+
+ Vector3f tmp1 = vars.vect6;
+
+ // calculate normals for each polygon
+ for (int r = 0; r < getHeight(); r++) {
+ for (int c = 0; c < getWidth(); c++) {
+
+ rootPoint.set(c, getValue(c, r), r);
+ Vector3f normal = vars.vect8;
+
+ if (r == 0) { // first row
+ if (c == 0) { // first column
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1);
+ getNormal(bottomPoint, rootPoint, rightPoint, scale, normal);
+ } else if (c == getWidth() - 1) { // last column
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1);
+ getNormal(leftPoint, rootPoint, bottomPoint, scale, normal);
+ } else { // all middle columns
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1);
+
+ normal.set( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
+ normal.add( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
+ normal.normalizeLocal();
+ }
+ } else if (r == getHeight() - 1) { // last row
+ if (c == 0) { // first column
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+ getNormal(rightPoint, rootPoint, topPoint, scale, normal);
+ } else if (c == getWidth() - 1) { // last column
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ getNormal(topPoint, rootPoint, leftPoint, scale, normal);
+ } else { // all middle columns
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+
+ normal.set( getNormal(topPoint, rootPoint, leftPoint, scale, tmp1) );
+ normal.add( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
+ normal.normalizeLocal();
+ }
+ } else { // all middle rows
+ if (c == 0) { // first column
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1);
+
+ normal.set( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
+ normal.add( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
+ normal.normalizeLocal();
+ } else if (c == getWidth() - 1) { // last column
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1); //XXX wrong
+
+ normal.set( getNormal(topPoint, rootPoint, leftPoint, scale, tmp1) );
+ normal.add( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
+ normal.normalizeLocal();
+ } else { // all middle columns
+ topPoint.set(c, getValue(c, r - 1), r - 1);
+ leftPoint.set(c - 1, getValue(c - 1, r), r);
+ rightPoint.set(c + 1, getValue(c + 1, r), r);
+ bottomPoint.set(c, getValue(c, r + 1), r + 1);
+
+ normal.set( getNormal(topPoint, rootPoint, leftPoint, scale, tmp1 ) );
+ normal.add( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
+ normal.add( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
+ normal.add( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
+ normal.normalizeLocal();
+ }
+ }
+
+ BufferUtils.setInBuffer(normal, store, (r * getWidth() + c)); // save the normal
+ }
+ }
+ vars.release();
+
+ return store;
+ }
+
+ private Vector3f getNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint, Vector3f scale, Vector3f store) {
+ float x1 = firstPoint.x - rootPoint.x;
+ float y1 = firstPoint.y - rootPoint.y;
+ float z1 = firstPoint.z - rootPoint.z;
+ x1 *= scale.x;
+ y1 *= scale.y;
+ z1 *= scale.z;
+ float x2 = secondPoint.x - rootPoint.x;
+ float y2 = secondPoint.y - rootPoint.y;
+ float z2 = secondPoint.z - rootPoint.z;
+ x2 *= scale.x;
+ y2 *= scale.y;
+ z2 *= scale.z;
+ float x3 = (y1 * z2) - (z1 * y2);
+ float y3 = (z1 * x2) - (x1 * z2);
+ float z3 = (x1 * y2) - (y1 * x2);
+
+ float inv = 1.0f / FastMath.sqrt(x3 * x3 + y3 * y3 + z3 * z3);
+ store.x = x3 * inv;
+ store.y = y3 * inv;
+ store.z = z3 * inv;
+ return store;
+
+ /*store.set( firstPoint.subtractLocal(rootPoint).multLocal(scale).crossLocal(secondPoint.subtractLocal(rootPoint).multLocal(scale)).normalizeLocal() );
+ return store;*/
+
+ }
+
+ /**
+ * Keeps a count of the number of indexes, good for debugging
+ */
+ public class VerboseIntBuffer {
+
+ private IntBuffer delegate;
+ int count = 0;
+
+ public VerboseIntBuffer(IntBuffer d) {
+ delegate = d;
+ }
+
+ public void put(int value) {
+ try {
+ delegate.put(value);
+ count++;
+ } catch (BufferOverflowException e) {
+ //System.out.println("err buffer size: "+delegate.capacity());
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+ }
+
+ /**
+ * Get a representation of the underlying triangle at the given point,
+ * translated to world coordinates.
+ *
+ * @param x local x coordinate
+ * @param z local z coordinate
+ * @return a triangle in world space not local space
+ */
+ protected Triangle getTriangleAtPoint(float x, float z, Vector3f scale, Vector3f translation) {
+ Triangle tri = getTriangleAtPoint(x, z);
+ if (tri != null) {
+ tri.get1().multLocal(scale).addLocal(translation);
+ tri.get2().multLocal(scale).addLocal(translation);
+ tri.get3().multLocal(scale).addLocal(translation);
+ }
+ return tri;
+ }
+
+ /**
+ * Get the two triangles that make up the grid section at the specified point,
+ * translated to world coordinates.
+ *
+ * @param x local x coordinate
+ * @param z local z coordinate
+ * @param scale
+ * @param translation
+ * @return two triangles in world space not local space
+ */
+ protected Triangle[] getGridTrianglesAtPoint(float x, float z, Vector3f scale, Vector3f translation) {
+ Triangle[] tris = getGridTrianglesAtPoint(x, z);
+ if (tris != null) {
+ tris[0].get1().multLocal(scale).addLocal(translation);
+ tris[0].get2().multLocal(scale).addLocal(translation);
+ tris[0].get3().multLocal(scale).addLocal(translation);
+ tris[1].get1().multLocal(scale).addLocal(translation);
+ tris[1].get2().multLocal(scale).addLocal(translation);
+ tris[1].get3().multLocal(scale).addLocal(translation);
+ }
+ return tris;
+ }
+
+ /**
+ * Get the two triangles that make up the grid section at the specified point.
+ *
+ * For every grid space there are two triangles oriented like this:
+ * *----*
+ * |a / |
+ * | / b|
+ * *----*
+ * The corners of the mesh have differently oriented triangles. The two
+ * corners that we have to special-case are the top left and bottom right
+ * corners. They are oriented inversely:
+ * *----*
+ * | \ b|
+ * |a \ |
+ * *----*
+ *
+ * @param x local x coordinate
+ * @param z local z coordinate
+ * @param scale
+ * @param translation
+ * @return
+ */
+ protected Triangle[] getGridTrianglesAtPoint(float x, float z) {
+ int gridX = (int) x;
+ int gridY = (int) z;
+
+ int index = findClosestHeightIndex(gridX, gridY);
+ if (index < 0) {
+ return null;
+ }
+
+ Triangle t = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());
+ Triangle t2 = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());
+
+ float h1 = hdata[index]; // top left
+ float h2 = hdata[index + 1]; // top right
+ float h3 = hdata[index + width]; // bottom left
+ float h4 = hdata[index + width + 1]; // bottom right
+
+
+ if ((gridX == 0 && gridY == 0) || (gridX == width - 1 && gridY == width - 1)) {
+ // top left or bottom right grid point
+ t.get(0).x = (gridX);
+ t.get(0).y = (h1);
+ t.get(0).z = (gridY);
+
+ t.get(1).x = (gridX);
+ t.get(1).y = (h3);
+ t.get(1).z = (gridY + 1);
+
+ t.get(2).x = (gridX + 1);
+ t.get(2).y = (h4);
+ t.get(2).z = (gridY + 1);
+
+ t2.get(0).x = (gridX);
+ t2.get(0).y = (h1);
+ t2.get(0).z = (gridY);
+
+ t2.get(1).x = (gridX + 1);
+ t2.get(1).y = (h4);
+ t2.get(1).z = (gridY + 1);
+
+ t2.get(2).x = (gridX + 1);
+ t2.get(2).y = (h2);
+ t2.get(2).z = (gridY);
+ } else {
+ // all other grid points
+ t.get(0).x = (gridX);
+ t.get(0).y = (h1);
+ t.get(0).z = (gridY);
+
+ t.get(1).x = (gridX);
+ t.get(1).y = (h3);
+ t.get(1).z = (gridY + 1);
+
+ t.get(2).x = (gridX + 1);
+ t.get(2).y = (h2);
+ t.get(2).z = (gridY);
+
+ t2.get(0).x = (gridX + 1);
+ t2.get(0).y = (h2);
+ t2.get(0).z = (gridY);
+
+ t2.get(1).x = (gridX);
+ t2.get(1).y = (h3);
+ t2.get(1).z = (gridY + 1);
+
+ t2.get(2).x = (gridX + 1);
+ t2.get(2).y = (h4);
+ t2.get(2).z = (gridY + 1);
+ }
+
+ return new Triangle[]{t, t2};
+ }
+
+ /**
+ * Get the triangle that the point is on.
+ *
+ * @param x coordinate in local space to the geomap
+ * @param z coordinate in local space to the geomap
+ * @return triangle in local space to the geomap
+ */
+ protected Triangle getTriangleAtPoint(float x, float z) {
+ Triangle[] triangles = getGridTrianglesAtPoint(x, z);
+ if (triangles == null) {
+ //System.out.println("x,z: " + x + "," + z);
+ return null;
+ }
+ Vector2f point = new Vector2f(x, z);
+ Vector2f t1 = new Vector2f(triangles[0].get1().x, triangles[0].get1().z);
+ Vector2f t2 = new Vector2f(triangles[0].get2().x, triangles[0].get2().z);
+ Vector2f t3 = new Vector2f(triangles[0].get3().x, triangles[0].get3().z);
+
+ if (0 != FastMath.pointInsideTriangle(t1, t2, t3, point)) {
+ return triangles[0];
+ }
+
+ t1.set(triangles[1].get1().x, triangles[1].get1().z);
+ t1.set(triangles[1].get2().x, triangles[1].get2().z);
+ t1.set(triangles[1].get3().x, triangles[1].get3().z);
+
+ if (0 != FastMath.pointInsideTriangle(t1, t2, t3, point)) {
+ return triangles[1];
+ }
+
+ return null;
+ }
+
+ protected int findClosestHeightIndex(int x, int z) {
+
+ if (x < 0 || x >= width - 1) {
+ return -1;
+ }
+ if (z < 0 || z >= width - 1) {
+ return -1;
+ }
+
+ return z * width + x;
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/LRUCache.java b/engine/src/terrain/com/jme3/terrain/geomipmap/LRUCache.java
new file mode 100644
index 0000000..5d2fde3
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/LRUCache.java
@@ -0,0 +1,122 @@
+package com.jme3.terrain.geomipmap;
+
+// Copyright 2007 Christian d'Heureuse, Inventec Informatik AG, Zurich,
+// Switzerland
+// www.source-code.biz, www.inventec.ch/chdh
+//
+// This module is multi-licensed and may be used under the terms
+// of any of the following licenses:
+//
+// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal
+// LGPL, GNU Lesser General Public License, V2 or later,
+// http://www.gnu.org/licenses/lgpl.html
+// GPL, GNU General Public License, V2 or later,
+// http://www.gnu.org/licenses/gpl.html
+// AL, Apache License, V2.0 or later, http://www.apache.org/licenses
+// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php
+//
+// Please contact the author if you need another license.
+// This module is provided "as is", without warranties of any kind.
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * An LRU cache, based on <code>LinkedHashMap</code>.
+ *
+ * <p>
+ * This cache has a fixed maximum number of elements (<code>cacheSize</code>).
+ * If the cache is full and another entry is added, the LRU (least recently
+ * used) entry is dropped.
+ *
+ * <p>
+ * This class is thread-safe. All methods of this class are synchronized.
+ *
+ * <p>
+ * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
+ * Multi-licensed: EPL / LGPL / GPL / AL / BSD.
+ */
+public class LRUCache<K, V> {
+
+ private static final float hashTableLoadFactor = 0.75f;
+ private LinkedHashMap<K, V> map;
+ private int cacheSize;
+
+ /**
+ * Creates a new LRU cache.
+ *
+ * @param cacheSize
+ * the maximum number of entries that will be kept in this cache.
+ */
+ public LRUCache(int cacheSize) {
+ this.cacheSize = cacheSize;
+ int hashTableCapacity = (int) Math.ceil(cacheSize / LRUCache.hashTableLoadFactor) + 1;
+ this.map = new LinkedHashMap<K, V>(hashTableCapacity, LRUCache.hashTableLoadFactor, true) {
+ // (an anonymous inner class)
+
+ private static final long serialVersionUID = 1;
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return this.size() > LRUCache.this.cacheSize;
+ }
+ };
+ }
+
+ /**
+ * Retrieves an entry from the cache.<br>
+ * The retrieved entry becomes the MRU (most recently used) entry.
+ *
+ * @param key
+ * the key whose associated value is to be returned.
+ * @return the value associated to this key, or null if no value with this
+ * key exists in the cache.
+ */
+ public synchronized V get(K key) {
+ return this.map.get(key);
+ }
+
+ /**
+ * Adds an entry to this cache.
+ * The new entry becomes the MRU (most recently used) entry.
+ * If an entry with the specified key already exists in the cache, it is
+ * replaced by the new entry.
+ * If the cache is full, the LRU (least recently used) entry is removed from
+ * the cache.
+ *
+ * @param key
+ * the key with which the specified value is to be associated.
+ * @param value
+ * a value to be associated with the specified key.
+ */
+ public synchronized void put(K key, V value) {
+ this.map.put(key, value);
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public synchronized void clear() {
+ this.map.clear();
+ }
+
+ /**
+ * Returns the number of used entries in the cache.
+ *
+ * @return the number of entries currently in the cache.
+ */
+ public synchronized int usedEntries() {
+ return this.map.size();
+ }
+
+ /**
+ * Returns a <code>Collection</code> that contains a copy of all cache
+ * entries.
+ *
+ * @return a <code>Collection</code> with a copy of the cache content.
+ */
+ public synchronized Collection<Map.Entry<K, V>> getAll() {
+ return new ArrayList<Map.Entry<K, V>>(this.map.entrySet());
+ }
+} // end class LRUCache
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/NormalRecalcControl.java b/engine/src/terrain/com/jme3/terrain/geomipmap/NormalRecalcControl.java
new file mode 100644
index 0000000..ba13e52
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/NormalRecalcControl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.control.Control;
+import java.io.IOException;
+
+
+/**
+ * Handles the normal vector updates when the terrain changes heights.
+ * @author bowens
+ */
+public class NormalRecalcControl extends AbstractControl {
+
+ private TerrainQuad terrain;
+
+ public NormalRecalcControl(){}
+
+ public NormalRecalcControl(TerrainQuad terrain) {
+ this.terrain = terrain;
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ terrain.updateNormals();
+ }
+
+ @Override
+ protected void controlRender(RenderManager rm, ViewPort vp) {
+
+ }
+
+ public Control cloneForSpatial(Spatial spatial) {
+ NormalRecalcControl control = new NormalRecalcControl(terrain);
+ control.setSpatial(spatial);
+ control.setEnabled(true);
+ return control;
+ }
+
+ @Override
+ public void setSpatial(Spatial spatial) {
+ super.setSpatial(spatial);
+ if (spatial instanceof TerrainQuad)
+ this.terrain = (TerrainQuad)spatial;
+ }
+
+ public TerrainQuad getTerrain() {
+ return terrain;
+ }
+
+ public void setTerrain(TerrainQuad terrain) {
+ this.terrain = terrain;
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(terrain, "terrain", null);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ terrain = (TerrainQuad) ic.readSavable("terrain", null);
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGrid.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGrid.java
new file mode 100644
index 0000000..502702e
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGrid.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.UpdateControl;
+import com.jme3.terrain.Terrain;
+import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
+import com.jme3.terrain.heightmap.HeightMap;
+import com.jme3.terrain.heightmap.HeightMapGrid;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * TerrainGrid itself is an actual TerrainQuad. Its four children are the visible four tiles.
+ *
+ * The grid is indexed by cells. Each cell has an integer XZ coordinate originating at 0,0.
+ * TerrainGrid will piggyback on the TerrainLodControl so it can use the camera for its
+ * updates as well. It does this in the overwritten update() method.
+ *
+ * It uses an LRU (Least Recently Used) cache of 16 terrain tiles (full TerrainQuadTrees). The
+ * center 4 are the ones that are visible. As the camera moves, it checks what camera cell it is in
+ * and will attach the now visible tiles.
+ *
+ * The 'quadIndex' variable is a 4x4 array that represents the tiles. The center
+ * four (index numbers: 5, 6, 9, 10) are what is visible. Each quadIndex value is an
+ * offset vector. The vector contains whole numbers and represents how many tiles in offset
+ * this location is from the center of the map. So for example the index 11 [Vector3f(2, 0, 1)]
+ * is located 2*terrainSize in X axis and 1*terrainSize in Z axis.
+ *
+ * As the camera moves, it tests what cameraCell it is in. Each camera cell covers four quad tiles
+ * and is half way inside each one.
+ *
+ * +-------+-------+
+ * | 1 | 4 | Four terrainQuads that make up the grid
+ * | *..|..* | with the cameraCell in the middle, covering
+ * |----|--|--|----| all four quads.
+ * | *..|..* |
+ * | 2 | 3 |
+ * +-------+-------+
+ *
+ * This results in the effect of when the camera gets half way across one of the sides of a quad to
+ * an empty (non-loaded) area, it will trigger the system to load in the next tiles.
+ *
+ * The tile loading is done on a background thread, and once the tile is loaded, then it is
+ * attached to the qrid quad tree, back on the OGL thread. It will grab the terrain quad from
+ * the LRU cache if it exists. If it does not exist, it will load in the new TerrainQuad tile.
+ *
+ * The loading of new tiles triggers events for any TerrainGridListeners. The events are:
+ * -tile Attached
+ * -tile Detached
+ * -grid moved.
+ *
+ * These allow physics to update, and other operation (often needed for loading the terrain) to occur
+ * at the right time.
+ *
+ * @author Anthyon
+ */
+public class TerrainGrid extends TerrainQuad {
+
+ protected static final Logger log = Logger.getLogger(TerrainGrid.class.getCanonicalName());
+ protected Vector3f currentCamCell = Vector3f.ZERO;
+ protected int quarterSize; // half of quadSize
+ protected int quadSize;
+ protected HeightMapGrid heightMapGrid;
+ private TerrainGridTileLoader gridTileLoader;
+ protected Vector3f[] quadIndex;
+ protected Set<TerrainGridListener> listeners = new HashSet<TerrainGridListener>();
+ protected Material material;
+ protected LRUCache<Vector3f, TerrainQuad> cache = new LRUCache<Vector3f, TerrainQuad>(16);
+ private int cellsLoaded = 0;
+ private int[] gridOffset;
+ private boolean runOnce = false;
+
+ protected class UpdateQuadCache implements Runnable {
+
+ protected final Vector3f location;
+
+ public UpdateQuadCache(Vector3f location) {
+ this.location = location;
+ }
+
+ /**
+ * This is executed if the camera has moved into a new CameraCell and will load in
+ * the new TerrainQuad tiles to be children of this TerrainGrid parent.
+ * It will first check the LRU cache to see if the terrain tile is already there,
+ * if it is not there, it will load it in and then cache that tile.
+ * The terrain tiles get added to the quad tree back on the OGL thread using the
+ * attachQuadAt() method. It also resets any cached values in TerrainQuad (such as
+ * neighbours).
+ */
+ public void run() {
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < 4; j++) {
+ int quadIdx = i * 4 + j;
+ final Vector3f quadCell = location.add(quadIndex[quadIdx]);
+ TerrainQuad q = cache.get(quadCell);
+ if (q == null) {
+ if (heightMapGrid != null) {
+ // create the new Quad since it doesn't exist
+ HeightMap heightMapAt = heightMapGrid.getHeightMapAt(quadCell);
+ q = new TerrainQuad(getName() + "Quad" + quadCell, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
+ q.setMaterial(material.clone());
+ log.log(Level.FINE, "Loaded TerrainQuad {0} from HeightMapGrid", q.getName());
+ } else if (gridTileLoader != null) {
+ q = gridTileLoader.getTerrainQuadAt(quadCell);
+ // only clone the material to the quad if it doesn't have a material of its own
+ if(q.getMaterial()==null) q.setMaterial(material.clone());
+ log.log(Level.FINE, "Loaded TerrainQuad {0} from TerrainQuadGrid", q.getName());
+ }
+ }
+ cache.put(quadCell, q);
+
+ if (isCenter(quadIdx)) {
+ // if it should be attached as a child right now, attach it
+ final int quadrant = getQuadrant(quadIdx);
+ final TerrainQuad newQuad = q;
+ // back on the OpenGL thread:
+ getControl(UpdateControl.class).enqueue(new Callable() {
+
+ public Object call() throws Exception {
+ attachQuadAt(newQuad, quadrant, quadCell);
+ //newQuad.resetCachedNeighbours();
+ return null;
+ }
+ });
+ }
+ }
+ }
+
+ }
+ }
+
+ protected boolean isCenter(int quadIndex) {
+ return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6;
+ }
+
+ protected int getQuadrant(int quadIndex) {
+ if (quadIndex == 5) {
+ return 1;
+ } else if (quadIndex == 9) {
+ return 2;
+ } else if (quadIndex == 6) {
+ return 3;
+ } else if (quadIndex == 10) {
+ return 4;
+ }
+ return 0; // error
+ }
+
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid,
+ Vector2f offset, float offsetAmount) {
+ this.name = name;
+ this.patchSize = patchSize;
+ this.size = maxVisibleSize;
+ this.stepScale = scale;
+ this.offset = offset;
+ this.offsetAmount = offsetAmount;
+ initData();
+ this.gridTileLoader = terrainQuadGrid;
+ terrainQuadGrid.setPatchSize(this.patchSize);
+ terrainQuadGrid.setQuadSize(this.quadSize);
+ addControl(new UpdateControl());
+ }
+
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid) {
+ this(name, patchSize, maxVisibleSize, scale, terrainQuadGrid, new Vector2f(), 0);
+ }
+
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) {
+ this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid);
+ }
+
+ @Deprecated
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid,
+ Vector2f offset, float offsetAmount) {
+ this.name = name;
+ this.patchSize = patchSize;
+ this.size = maxVisibleSize;
+ this.stepScale = scale;
+ this.offset = offset;
+ this.offsetAmount = offsetAmount;
+ initData();
+ this.heightMapGrid = heightMapGrid;
+ heightMapGrid.setSize(this.quadSize);
+ addControl(new UpdateControl());
+ }
+
+ @Deprecated
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid) {
+ this(name, patchSize, maxVisibleSize, scale, heightMapGrid, new Vector2f(), 0);
+ }
+
+ @Deprecated
+ public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) {
+ this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid);
+ }
+
+ public TerrainGrid() {
+ }
+
+ private void initData() {
+ int maxVisibleSize = size;
+ this.quarterSize = maxVisibleSize >> 2;
+ this.quadSize = (maxVisibleSize + 1) >> 1;
+ this.totalSize = maxVisibleSize;
+ this.gridOffset = new int[]{0, 0};
+
+ /*
+ * -z
+ * |
+ * 1|3
+ * -x ----+---- x
+ * 2|4
+ * |
+ * z
+ */
+ this.quadIndex = new Vector3f[]{
+ new Vector3f(-1, 0, -1), new Vector3f(0, 0, -1), new Vector3f(1, 0, -1), new Vector3f(2, 0, -1),
+ new Vector3f(-1, 0, 0), new Vector3f(0, 0, 0), new Vector3f(1, 0, 0), new Vector3f(2, 0, 0),
+ new Vector3f(-1, 0, 1), new Vector3f(0, 0, 1), new Vector3f(1, 0, 1), new Vector3f(2, 0, 1),
+ new Vector3f(-1, 0, 2), new Vector3f(0, 0, 2), new Vector3f(1, 0, 2), new Vector3f(2, 0, 2)};
+
+ }
+
+ /**
+ * @deprecated not needed to be called any more, handled automatically
+ */
+ public void initialize(Vector3f location) {
+ if (this.material == null) {
+ throw new RuntimeException("Material must be set prior to call of initialize");
+ }
+ Vector3f camCell = this.getCamCell(location);
+ this.updateChildren(camCell);
+ for (TerrainGridListener l : this.listeners) {
+ l.gridMoved(camCell);
+ }
+ }
+
+ @Override
+ public void update(List<Vector3f> locations, LodCalculator lodCalculator) {
+ // for now, only the first camera is handled.
+ // to accept more, there are two ways:
+ // 1: every camera has an associated grid, then the location is not enough to identify which camera location has changed
+ // 2: grids are associated with locations, and no incremental update is done, we load new grids for new locations, and unload those that are not needed anymore
+ Vector3f cam = locations.isEmpty() ? Vector3f.ZERO.clone() : locations.get(0);
+ Vector3f camCell = this.getCamCell(cam); // get the grid index value of where the camera is (ie. 2,1)
+ if (cellsLoaded > 1) { // Check if cells are updated before updating gridoffset.
+ gridOffset[0] = Math.round(camCell.x * (size / 2));
+ gridOffset[1] = Math.round(camCell.z * (size / 2));
+ cellsLoaded = 0;
+ }
+ if (camCell.x != this.currentCamCell.x || camCell.z != currentCamCell.z || !runOnce) {
+ // if the camera has moved into a new cell, load new terrain into the visible 4 center quads
+ this.updateChildren(camCell);
+ for (TerrainGridListener l : this.listeners) {
+ l.gridMoved(camCell);
+ }
+ }
+ runOnce = true;
+ super.update(locations, lodCalculator);
+ }
+
+ public Vector3f getCamCell(Vector3f location) {
+ Vector3f tile = getTileCell(location);
+ Vector3f offsetHalf = new Vector3f(-0.5f, 0, -0.5f);
+ Vector3f shifted = tile.subtract(offsetHalf);
+ return new Vector3f(FastMath.floor(shifted.x), 0, FastMath.floor(shifted.z));
+ }
+
+ /**
+ * Centered at 0,0.
+ * Get the tile index location in integer form:
+ * @param location world coordinate
+ */
+ public Vector3f getTileCell(Vector3f location) {
+ Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize));
+ return tileLoc;
+ }
+
+ public TerrainGridTileLoader getGridTileLoader() {
+ return gridTileLoader;
+ }
+
+ protected void removeQuad(int idx) {
+ if (this.getQuad(idx) != null) {
+ for (TerrainGridListener l : listeners) {
+ l.tileDetached(getTileCell(this.getQuad(idx).getWorldTranslation()), this.getQuad(idx));
+ }
+ this.detachChild(this.getQuad(idx));
+ cellsLoaded++; // For gridoffset calc., maybe the run() method is a better location for this.
+ }
+ }
+
+ /**
+ * Runs on the rendering thread
+ */
+ protected void attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell) {
+ this.removeQuad(quadrant);
+
+ q.setQuadrant((short) quadrant);
+ this.attachChild(q);
+
+ Vector3f loc = quadCell.mult(this.quadSize - 1).subtract(quarterSize, 0, quarterSize);// quadrant location handled TerrainQuad automatically now
+ q.setLocalTranslation(loc);
+
+ for (TerrainGridListener l : listeners) {
+ l.tileAttached(quadCell, q);
+ }
+ updateModelBound();
+
+ for (Spatial s : getChildren()) {
+ if (s instanceof TerrainQuad) {
+ TerrainQuad tq = (TerrainQuad)s;
+ tq.resetCachedNeighbours();
+ tq.fixNormalEdges(new BoundingBox(tq.getWorldTranslation(), totalSize*2, Float.MAX_VALUE, totalSize*2));
+ }
+ }
+ }
+
+ @Deprecated
+ /**
+ * @Deprecated, use updateChildren
+ */
+ protected void updateChildrens(Vector3f camCell) {
+ updateChildren(camCell);
+ }
+
+ /**
+ * Called when the camera has moved into a new cell. We need to
+ * update what quads are in the scene now.
+ *
+ * Step 1: touch cache
+ * LRU cache is used, so elements that need to remain
+ * should be touched.
+ *
+ * Step 2: load new quads in background thread
+ * if the camera has moved into a new cell, we load in new quads
+ * @param camCell the cell the camera is in
+ */
+ protected void updateChildren(Vector3f camCell) {
+
+ int dx = 0;
+ int dy = 0;
+ if (currentCamCell != null) {
+ dx = (int) (camCell.x - currentCamCell.x);
+ dy = (int) (camCell.z - currentCamCell.z);
+ }
+
+ int xMin = 0;
+ int xMax = 4;
+ int yMin = 0;
+ int yMax = 4;
+ if (dx == -1) { // camera moved to -X direction
+ xMax = 3;
+ } else if (dx == 1) { // camera moved to +X direction
+ xMin = 1;
+ }
+
+ if (dy == -1) { // camera moved to -Y direction
+ yMax = 3;
+ } else if (dy == 1) { // camera moved to +Y direction
+ yMin = 1;
+ }
+
+ // Touch the items in the cache that we are and will be interested in.
+ // We activate cells in the direction we are moving. If we didn't move
+ // either way in one of the axes (say X or Y axis) then they are all touched.
+ for (int i = yMin; i < yMax; i++) {
+ for (int j = xMin; j < xMax; j++) {
+ cache.get(camCell.add(quadIndex[i * 4 + j]));
+ }
+ }
+ // ---------------------------------------------------
+ // ---------------------------------------------------
+
+ if (executor == null) {
+ // use the same executor as the LODControl
+ executor = createExecutorService();
+ }
+
+ executor.submit(new UpdateQuadCache(camCell));
+
+ this.currentCamCell = camCell;
+ }
+
+ public void addListener(TerrainGridListener listener) {
+ this.listeners.add(listener);
+ }
+
+ public Vector3f getCurrentCell() {
+ return this.currentCamCell;
+ }
+
+ public void removeListener(TerrainGridListener listener) {
+ this.listeners.remove(listener);
+ }
+
+ @Override
+ public void setMaterial(Material mat) {
+ this.material = mat;
+ super.setMaterial(mat);
+ }
+
+ public void setQuadSize(int quadSize) {
+ this.quadSize = quadSize;
+ }
+
+ @Override
+ public void adjustHeight(List<Vector2f> xz, List<Float> height) {
+ Vector3f currentGridLocation = getCurrentCell().mult(getLocalScale()).multLocal(quadSize - 1);
+ for (Vector2f vect : xz) {
+ vect.x -= currentGridLocation.x;
+ vect.y -= currentGridLocation.z;
+ }
+ super.adjustHeight(xz, height);
+ }
+
+ @Override
+ protected float getHeightmapHeight(int x, int z) {
+ return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]);
+ }
+
+ @Override
+ public int getNumMajorSubdivisions() {
+ return 2;
+ }
+
+ @Override
+ public Material getMaterial(Vector3f worldLocation) {
+ if (worldLocation == null)
+ return null;
+ Vector3f tileCell = getTileCell(worldLocation);
+ Terrain terrain = cache.get(tileCell);
+ if (terrain == null)
+ return null; // terrain not loaded for that cell yet!
+ return terrain.getMaterial(worldLocation);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule c = im.getCapsule(this);
+ name = c.readString("name", null);
+ size = c.readInt("size", 0);
+ patchSize = c.readInt("patchSize", 0);
+ stepScale = (Vector3f) c.readSavable("stepScale", null);
+ offset = (Vector2f) c.readSavable("offset", null);
+ offsetAmount = c.readFloat("offsetAmount", 0);
+ gridTileLoader = (TerrainGridTileLoader) c.readSavable("terrainQuadGrid", null);
+ material = (Material) c.readSavable("material", null);
+ initData();
+ if (gridTileLoader != null) {
+ gridTileLoader.setPatchSize(this.patchSize);
+ gridTileLoader.setQuadSize(this.quadSize);
+ }
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule c = ex.getCapsule(this);
+ c.write(gridTileLoader, "terrainQuadGrid", null);
+ c.write(size, "size", 0);
+ c.write(patchSize, "patchSize", 0);
+ c.write(stepScale, "stepScale", null);
+ c.write(offset, "offset", null);
+ c.write(offsetAmount, "offsetAmount", 0);
+ c.write(material, "material", null);
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridListener.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridListener.java
new file mode 100644
index 0000000..e972ee0
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.math.Vector3f;
+
+/**
+ *
+ * @author Anthyon
+ */
+public interface TerrainGridListener {
+
+ public void gridMoved(Vector3f newCenter);
+
+ public void tileAttached( Vector3f cell, TerrainQuad quad );
+
+ public void tileDetached( Vector3f cell, TerrainQuad quad );
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridTileLoader.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridTileLoader.java
new file mode 100644
index 0000000..485f51f
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainGridTileLoader.java
@@ -0,0 +1,21 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+
+/**
+ *
+ * @author normenhansen
+ */
+public interface TerrainGridTileLoader extends Savable {
+
+ public TerrainQuad getTerrainQuadAt(Vector3f location);
+
+ public void setPatchSize(int patchSize);
+
+ public void setQuadSize(int quadSize);
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainLodControl.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainLodControl.java
new file mode 100644
index 0000000..aedd744
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainLodControl.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.control.Control;
+import com.jme3.terrain.Terrain;
+import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
+import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tells the terrain to update its Level of Detail.
+ * It needs the cameras to do this, and there could possibly
+ * be several cameras in the scene, so it accepts a list
+ * of cameras.
+ * NOTE: right now it just uses the first camera passed in,
+ * in the future it will use all of them to determine what
+ * LOD to set.
+ *
+ * This control serializes, but it does not save the Camera reference.
+ * This camera reference has to be manually added in when you load the
+ * terrain to the scene!
+ *
+ * @author Brent Owens
+ */
+public class TerrainLodControl extends AbstractControl {
+
+ private Terrain terrain;
+ private List<Camera> cameras;
+ private List<Vector3f> cameraLocations = new ArrayList<Vector3f>();
+ private LodCalculator lodCalculator;
+ private boolean hasResetLod = false; // used when enabled is set to false
+
+ public TerrainLodControl() {
+ }
+
+ public TerrainLodControl(Terrain terrain, Camera camera) {
+ List<Camera> cams = new ArrayList<Camera>();
+ cams.add(camera);
+ this.terrain = terrain;
+ this.cameras = cams;
+ lodCalculator = new DistanceLodCalculator(65, 2.7f); // a default calculator
+ }
+
+ /**
+ * Only uses the first camera right now.
+ * @param terrain to act upon (must be a Spatial)
+ * @param cameras one or more cameras to reference for LOD calc
+ */
+ public TerrainLodControl(Terrain terrain, List<Camera> cameras) {
+ this.terrain = terrain;
+ this.cameras = cameras;
+ lodCalculator = new DistanceLodCalculator(65, 2.7f); // a default calculator
+ }
+
+ @Override
+ protected void controlRender(RenderManager rm, ViewPort vp) {
+ }
+
+ @Override
+ public void update(float tpf) {
+ controlUpdate(tpf);
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ //list of cameras for when terrain supports multiple cameras (ie split screen)
+
+ if (lodCalculator == null)
+ return;
+
+ if (!enabled) {
+ if (!hasResetLod) {
+ // this will get run once
+ hasResetLod = true;
+ lodCalculator.turnOffLod();
+ }
+ }
+
+ if (cameras != null) {
+ if (cameraLocations.isEmpty() && !cameras.isEmpty()) {
+ for (Camera c : cameras) // populate them
+ {
+ cameraLocations.add(c.getLocation());
+ }
+ }
+ terrain.update(cameraLocations, lodCalculator);
+ }
+ }
+
+ public Control cloneForSpatial(Spatial spatial) {
+ if (spatial instanceof Terrain) {
+ List<Camera> cameraClone = new ArrayList<Camera>();
+ if (cameras != null) {
+ for (Camera c : cameras) {
+ cameraClone.add(c);
+ }
+ }
+ TerrainLodControl cloned = new TerrainLodControl((Terrain) spatial, cameraClone);
+ cloned.setLodCalculator(lodCalculator.clone());
+ return cloned;
+ }
+ return null;
+ }
+
+ public void setCamera(Camera camera) {
+ List<Camera> cams = new ArrayList<Camera>();
+ cams.add(camera);
+ setCameras(cams);
+ }
+
+ public void setCameras(List<Camera> cameras) {
+ this.cameras = cameras;
+ cameraLocations.clear();
+ for (Camera c : cameras) {
+ cameraLocations.add(c.getLocation());
+ }
+ }
+
+ @Override
+ public void setSpatial(Spatial spatial) {
+ super.setSpatial(spatial);
+ if (spatial instanceof Terrain) {
+ this.terrain = (Terrain) spatial;
+ }
+ }
+
+ public void setTerrain(Terrain terrain) {
+ this.terrain = terrain;
+ }
+
+ public LodCalculator getLodCalculator() {
+ return lodCalculator;
+ }
+
+ public void setLodCalculator(LodCalculator lodCalculator) {
+ this.lodCalculator = lodCalculator;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (!enabled) {
+ // reset the lod levels to max detail for the terrain
+ hasResetLod = false;
+ } else {
+ hasResetLod = true;
+ lodCalculator.turnOnLod();
+ }
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write((Node)terrain, "terrain", null);
+ oc.write(lodCalculator, "lodCalculator", null);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ terrain = (Terrain) ic.readSavable("terrain", null);
+ lodCalculator = (LodCalculator) ic.readSavable("lodCalculator", new DistanceLodCalculator());
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java
new file mode 100644
index 0000000..40cf190
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java
@@ -0,0 +1,983 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bounding.BoundingSphere;
+import com.jme3.bounding.BoundingVolume;
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResults;
+import com.jme3.collision.UnsupportedCollisionException;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.*;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.terrain.geomipmap.TerrainQuad.LocationHeight;
+import com.jme3.terrain.geomipmap.lodcalc.util.EntropyComputeUtil;
+import com.jme3.util.BufferUtils;
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * A terrain patch is a leaf in the terrain quad tree. It has a mesh that can change levels of detail (LOD)
+ * whenever the view point, or camera, changes. The actual terrain mesh is created by the LODGeomap class.
+ * That uses a geo-mipmapping algorithm to change the index buffer of the mesh.
+ * The mesh is a triangle strip. In wireframe mode you might notice some strange lines, these are degenerate
+ * triangles generated by the geoMipMap algorithm and can be ignored. The video card removes them at almost no cost.
+ *
+ * Each patch needs to know its neighbour's LOD so it can seam its edges with them, in case the neighbour has a different
+ * LOD. If this doesn't happen, you will see gaps.
+ *
+ * The LOD value is most detailed at zero. It gets less detailed the higher the LOD value until you reach maxLod, which
+ * is a mathematical limit on the number of times the 'size' of the patch can be divided by two. However there is a -1 to that
+ * for now until I add in a custom index buffer calculation for that max level, the current algorithm does not go that far.
+ *
+ * You can supply a LodThresholdCalculator for use in determining when the LOD should change. It's API will no doubt change
+ * in the near future. Right now it defaults to just changing LOD every two patch sizes. So if a patch has a size of 65,
+ * then the LOD changes every 130 units away.
+ *
+ * @author Brent Owens
+ */
+public class TerrainPatch extends Geometry {
+
+ protected LODGeomap geomap;
+ protected int lod = -1; // this terrain patch's LOD
+ private int maxLod = -1;
+ protected int previousLod = -1;
+ protected int lodLeft, lodTop, lodRight, lodBottom; // it's neighbour's LODs
+
+ protected int size;
+
+ protected int totalSize;
+
+ protected short quadrant = 1;
+
+ // x/z step
+ protected Vector3f stepScale;
+
+ // center of the patch in relation to (0,0,0)
+ protected Vector2f offset;
+
+ // amount the patch has been shifted.
+ protected float offsetAmount;
+
+ //protected LodCalculator lodCalculator;
+ //protected LodCalculatorFactory lodCalculatorFactory;
+
+ protected TerrainPatch leftNeighbour, topNeighbour, rightNeighbour, bottomNeighbour;
+ protected boolean searchedForNeighboursAlready = false;
+
+
+ protected float[] lodEntropy;
+
+ public TerrainPatch() {
+ super("TerrainPatch");
+ }
+
+ public TerrainPatch(String name) {
+ super(name);
+ }
+
+ public TerrainPatch(String name, int size) {
+ this(name, size, new Vector3f(1,1,1), null, new Vector3f(0,0,0));
+ }
+
+ /**
+ * Constructor instantiates a new <code>TerrainPatch</code> object. The
+ * parameters and heightmap data are then processed to generate a
+ * <code>TriMesh</code> object for rendering.
+ *
+ * @param name
+ * the name of the terrain patch.
+ * @param size
+ * the size of the heightmap.
+ * @param stepScale
+ * the scale for the axes.
+ * @param heightMap
+ * the height data.
+ * @param origin
+ * the origin offset of the patch.
+ */
+ public TerrainPatch(String name, int size, Vector3f stepScale,
+ float[] heightMap, Vector3f origin) {
+ this(name, size, stepScale, heightMap, origin, size, new Vector2f(), 0);
+ }
+
+ /**
+ * Constructor instantiates a new <code>TerrainPatch</code> object. The
+ * parameters and heightmap data are then processed to generate a
+ * <code>TriMesh</code> object for renderering.
+ *
+ * @param name
+ * the name of the terrain patch.
+ * @param size
+ * the size of the patch.
+ * @param stepScale
+ * the scale for the axes.
+ * @param heightMap
+ * the height data.
+ * @param origin
+ * the origin offset of the patch.
+ * @param totalSize
+ * the total size of the terrain. (Higher if the patch is part of
+ * a <code>TerrainQuad</code> tree.
+ * @param offset
+ * the offset for texture coordinates.
+ * @param offsetAmount
+ * the total offset amount. Used for texture coordinates.
+ */
+ public TerrainPatch(String name, int size, Vector3f stepScale,
+ float[] heightMap, Vector3f origin, int totalSize,
+ Vector2f offset, float offsetAmount) {
+ super(name);
+ this.size = size;
+ this.stepScale = stepScale;
+ this.totalSize = totalSize;
+ this.offsetAmount = offsetAmount;
+ this.offset = offset;
+
+ setLocalTranslation(origin);
+
+ geomap = new LODGeomap(size, heightMap);
+ Mesh m = geomap.createMesh(stepScale, new Vector2f(1,1), offset, offsetAmount, totalSize, false);
+ setMesh(m);
+
+ }
+
+ /**
+ * This calculation is slow, so don't use it often.
+ */
+ public void generateLodEntropies() {
+ float[] entropies = new float[getMaxLod()+1];
+ for (int i = 0; i <= getMaxLod(); i++){
+ int curLod = (int) Math.pow(2, i);
+ IntBuffer buf = geomap.writeIndexArrayLodDiff(null, curLod, false, false, false, false);
+ entropies[i] = EntropyComputeUtil.computeLodEntropy(mesh, buf);
+ }
+
+ lodEntropy = entropies;
+ }
+
+ public float[] getLodEntropies(){
+ if (lodEntropy == null){
+ generateLodEntropies();
+ }
+ return lodEntropy;
+ }
+
+ @Deprecated
+ public FloatBuffer getHeightmap() {
+ return BufferUtils.createFloatBuffer(geomap.getHeightArray());
+ }
+
+ public float[] getHeightMap() {
+ return geomap.getHeightArray();
+ }
+
+ /**
+ * The maximum lod supported by this terrain patch.
+ * If the patch size is 32 then the returned value would be log2(32)-2 = 3
+ * You can then use that value, 3, to see how many times you can divide 32 by 2
+ * before the terrain gets too un-detailed (can't stitch it any further).
+ * @return
+ */
+ public int getMaxLod() {
+ if (maxLod < 0)
+ maxLod = Math.max(1, (int) (FastMath.log(size-1)/FastMath.log(2)) -1); // -1 forces our minimum of 4 triangles wide
+
+ return maxLod;
+ }
+
+ protected void reIndexGeometry(HashMap<String,UpdatedTerrainPatch> updated, boolean useVariableLod) {
+
+ UpdatedTerrainPatch utp = updated.get(getName());
+
+ if (utp != null && (utp.isReIndexNeeded() || utp.isFixEdges()) ) {
+ int pow = (int) Math.pow(2, utp.getNewLod());
+ boolean left = utp.getLeftLod() > utp.getNewLod();
+ boolean top = utp.getTopLod() > utp.getNewLod();
+ boolean right = utp.getRightLod() > utp.getNewLod();
+ boolean bottom = utp.getBottomLod() > utp.getNewLod();
+
+ IntBuffer ib = null;
+ if (useVariableLod)
+ ib = geomap.writeIndexArrayLodVariable(null, pow, (int) Math.pow(2, utp.getRightLod()), (int) Math.pow(2, utp.getTopLod()), (int) Math.pow(2, utp.getLeftLod()), (int) Math.pow(2, utp.getBottomLod()));
+ else
+ ib = geomap.writeIndexArrayLodDiff(null, pow, right, top, left, bottom);
+ utp.setNewIndexBuffer(ib);
+ }
+
+ }
+
+
+ public Vector2f getTex(float x, float z, Vector2f store) {
+ if (x < 0 || z < 0 || x >= size || z >= size) {
+ store.set(Vector2f.ZERO);
+ return store;
+ }
+ int idx = (int) (z * size + x);
+ return store.set(getMesh().getFloatBuffer(Type.TexCoord).get(idx*2),
+ getMesh().getFloatBuffer(Type.TexCoord).get(idx*2+1) );
+ }
+
+ public float getHeightmapHeight(float x, float z) {
+ if (x < 0 || z < 0 || x >= size || z >= size)
+ return 0;
+ int idx = (int) (z * size + x);
+ return getMesh().getFloatBuffer(Type.Position).get(idx*3+1); // 3 floats per entry (x,y,z), the +1 is to get the Y
+ }
+
+ /**
+ * Get the triangle of this geometry at the specified local coordinate.
+ * @param x local to the terrain patch
+ * @param z local to the terrain patch
+ * @return the triangle in world coordinates, or null if the point does intersect this patch on the XZ axis
+ */
+ public Triangle getTriangle(float x, float z) {
+ return geomap.getTriangleAtPoint(x, z, getWorldScale() , getWorldTranslation());
+ }
+
+ /**
+ * Get the triangles at the specified grid point. Probably only 2 triangles
+ * @param x local to the terrain patch
+ * @param z local to the terrain patch
+ * @return the triangles in world coordinates, or null if the point does intersect this patch on the XZ axis
+ */
+ public Triangle[] getGridTriangles(float x, float z) {
+ return geomap.getGridTrianglesAtPoint(x, z, getWorldScale() , getWorldTranslation());
+ }
+
+ protected void setHeight(List<LocationHeight> locationHeights, boolean overrideHeight) {
+
+ for (LocationHeight lh : locationHeights) {
+ if (lh.x < 0 || lh.z < 0 || lh.x >= size || lh.z >= size)
+ continue;
+ int idx = lh.z * size + lh.x;
+ if (overrideHeight) {
+ geomap.getHeightArray()[idx] = lh.h;
+ } else {
+ float h = getMesh().getFloatBuffer(Type.Position).get(idx*3+1);
+ geomap.getHeightArray()[idx] = h+lh.h;
+ }
+
+ }
+
+ FloatBuffer newVertexBuffer = geomap.writeVertexArray(null, stepScale, false);
+ getMesh().clearBuffer(Type.Position);
+ getMesh().setBuffer(Type.Position, 3, newVertexBuffer);
+ }
+
+ /**
+ * recalculate all of the normal vectors in this terrain patch
+ */
+ protected void updateNormals() {
+ FloatBuffer newNormalBuffer = geomap.writeNormalArray(null, getWorldScale());
+ getMesh().getBuffer(Type.Normal).updateData(newNormalBuffer);
+ FloatBuffer newTangentBuffer = null;
+ FloatBuffer newBinormalBuffer = null;
+ FloatBuffer[] tb = geomap.writeTangentArray(newTangentBuffer, newBinormalBuffer, (FloatBuffer)getMesh().getBuffer(Type.TexCoord).getData(), getWorldScale());
+ newTangentBuffer = tb[0];
+ newBinormalBuffer = tb[1];
+ getMesh().getBuffer(Type.Tangent).updateData(newTangentBuffer);
+ getMesh().getBuffer(Type.Binormal).updateData(newBinormalBuffer);
+ }
+
+ /**
+ * Matches the normals along the edge of the patch with the neighbours.
+ * Computes the normals for the right, bottom, left, and top edges of the
+ * patch, and saves those normals in the neighbour's edges too.
+ *
+ * Takes 4 points (if has neighbour on that side) for each
+ * point on the edge of the patch:
+ * *
+ * |
+ * *---x---*
+ * |
+ * *
+ * It works across the right side of the patch, from the top down to
+ * the bottom. Then it works on the bottom side of the patch, from the
+ * left to the right.
+ */
+ protected void fixNormalEdges(TerrainPatch right,
+ TerrainPatch bottom,
+ TerrainPatch top,
+ TerrainPatch left,
+ TerrainPatch bottomRight,
+ TerrainPatch bottomLeft,
+ TerrainPatch topRight,
+ TerrainPatch topLeft)
+ {
+ Vector3f rootPoint = new Vector3f();
+ Vector3f rightPoint = new Vector3f();
+ Vector3f leftPoint = new Vector3f();
+ Vector3f topPoint = new Vector3f();
+
+ Vector3f bottomPoint = new Vector3f();
+
+ Vector3f tangent = new Vector3f();
+ Vector3f binormal = new Vector3f();
+ Vector3f normal = new Vector3f();
+
+ int s = this.getSize()-1;
+
+ if (right != null) { // right side, works its way down
+ for (int i=0; i<s+1; i++) {
+ rootPoint.set(s, this.getHeightmapHeight(s,i), i);
+ leftPoint.set(s-1, this.getHeightmapHeight(s-1,i), i);
+ rightPoint.set(s+1, right.getHeightmapHeight(1,i), i);
+
+ if (i == 0) { // top point
+ if (top == null) {
+ bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
+
+ averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), s);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), 0);
+ } else {
+ topPoint.set(s, top.getHeightmapHeight(s,s-1), i-1);
+ bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), s);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), 0);
+
+ if (topRight != null) {
+ VertexBuffer topRightNB = topRight.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)topRightNB.getData(), (s+1)*s);
+ topRightNB.setUpdateNeeded();
+ }
+ }
+ } else if (i == s) { // bottom point
+ if (bottom == null) {
+ topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i+1)-1);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*(s));
+ } else {
+ topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
+ bottomPoint.set(s, bottom.getHeightmapHeight(s,1), i+1);
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
+ VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
+
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s+1)-1);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*s);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), s);
+
+ if (bottomRight != null) {
+ VertexBuffer bottomRightNB = bottomRight.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)bottomRightNB.getData(), 0);
+ bottomRightNB.setUpdateNeeded();
+ }
+ downNB.setUpdateNeeded();
+ }
+ } else { // all in the middle
+ topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
+ bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i+1)-1);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*(i));
+ }
+ }
+ right.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
+ }
+
+ if (left != null) { // left side, works its way down
+ for (int i=0; i<s+1; i++) {
+ rootPoint.set(0, this.getHeightmapHeight(0,i), i);
+ leftPoint.set(-1, left.getHeightmapHeight(s-1,i), i);
+ rightPoint.set(1, this.getHeightmapHeight(1,i), i);
+
+ if (i == 0) { // top point
+ if (top == null) {
+ bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
+ averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), 0);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), s);
+ } else {
+ topPoint.set(0, top.getHeightmapHeight(0,s-1), i-1);
+ bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), 0);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), s);
+
+ if (topLeft != null) {
+ VertexBuffer topLeftNB = topLeft.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)topLeftNB.getData(), (s+1)*(s+1)-1);
+ topLeftNB.setUpdateNeeded();
+ }
+ }
+ } else if (i == s) { // bottom point
+ if (bottom == null) {
+ topPoint.set(0, this.getHeightmapHeight(0,i-1), i-1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s));
+ BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), (s+1)*(i+1)-1);
+ } else {
+ topPoint.set(0, this.getHeightmapHeight(0,i-1), i-1);
+ bottomPoint.set(0, bottom.getHeightmapHeight(0,1), i+1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
+ VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
+
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s));
+ BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), (s+1)*(i+1)-1);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), 0);
+
+ if (bottomLeft != null) {
+ VertexBuffer bottomLeftNB = bottomLeft.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)bottomLeftNB.getData(), s);
+ bottomLeftNB.setUpdateNeeded();
+ }
+ downNB.setUpdateNeeded();
+ }
+ } else { // all in the middle
+ topPoint.set(0, this.getHeightmapHeight(0,i-1), i-1);
+ bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
+
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i));
+ BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), (s+1)*(i+1)-1);
+ }
+ }
+ left.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
+ }
+
+ if (top != null) { // top side, works its way right
+ for (int i=0; i<s+1; i++) {
+ rootPoint.set(i, this.getHeightmapHeight(i,0), 0);
+ topPoint.set(i, top.getHeightmapHeight(i,s-1), -1);
+ bottomPoint.set(i, this.getHeightmapHeight(i,1), 1);
+
+ if (i == 0) { // left corner
+ // handled by left side pass
+
+ } else if (i == s) { // right corner
+
+ // handled by this patch when it does its right side
+
+ } else { // all in the middle
+ leftPoint.set(i-1, this.getHeightmapHeight(i-1,0), 0);
+ rightPoint.set(i+1, this.getHeightmapHeight(i+1,0), 0);
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), i);
+ VertexBuffer topNB = top.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)topNB.getData(), (s+1)*(s)+i);
+ }
+ }
+ top.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
+
+ }
+
+ if (bottom != null) { // bottom side, works its way right
+ for (int i=0; i<s+1; i++) {
+ rootPoint.set(i, this.getHeightmapHeight(i,s), s);
+ topPoint.set(i, this.getHeightmapHeight(i,s-1), s-1);
+ bottomPoint.set(i, bottom.getHeightmapHeight(i,1), s+1);
+
+ if (i == 0) { // left
+ // handled by the left side pass
+
+ } else if (i == s) { // right
+
+ // handled by this patch when it does its right side
+
+ } else { // all in the middle
+ leftPoint.set(i-1, this.getHeightmapHeight(i-1,s), s);
+ rightPoint.set(i+1, this.getHeightmapHeight(i+1,s), s);
+ averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, null, null, null, null, null, normal, tangent, binormal);
+ VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s)+i);
+ VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
+ BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), i);
+ }
+ }
+ bottom.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
+
+ }
+
+ this.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
+ this.getMesh().getBuffer(Type.Tangent).setUpdateNeeded();
+ this.getMesh().getBuffer(Type.Binormal).setUpdateNeeded();
+ }
+
+ protected void averageNormalsTangents(
+ Vector3f topPoint,
+ Vector3f rootPoint,
+ Vector3f leftPoint,
+ Vector3f bottomPoint,
+ Vector3f rightPoint,
+ Vector2f topTex,
+ Vector2f rootTex,
+ Vector2f leftTex,
+ Vector2f bottomTex,
+ Vector2f rightTex,
+ Vector3f normal,
+ Vector3f tangent,
+ Vector3f binormal)
+ {
+ Vector3f scale = getWorldScale();
+
+ Vector3f n1 = Vector3f.ZERO;
+ if (topPoint != null && leftPoint != null) {
+ n1 = calculateNormal(topPoint.mult(scale), rootPoint.mult(scale), leftPoint.mult(scale));
+ }
+ Vector3f n2 = Vector3f.ZERO;
+ if (leftPoint != null && bottomPoint != null) {
+ n2 = calculateNormal(leftPoint.mult(scale), rootPoint.mult(scale), bottomPoint.mult(scale));
+ }
+ Vector3f n3 = Vector3f.ZERO;
+ if (rightPoint != null && bottomPoint != null) {
+ n3 = calculateNormal(bottomPoint.mult(scale), rootPoint.mult(scale), rightPoint.mult(scale));
+ }
+ Vector3f n4 = Vector3f.ZERO;
+ if (rightPoint != null && topPoint != null) {
+ n4 = calculateNormal(rightPoint.mult(scale), rootPoint.mult(scale), topPoint.mult(scale));
+ }
+
+ if (bottomPoint != null && rightPoint != null && rootTex != null && rightTex != null && bottomTex != null)
+ LODGeomap.calculateTangent(new Vector3f[]{rootPoint.mult(scale),rightPoint.mult(scale),bottomPoint.mult(scale)}, new Vector2f[]{rootTex,rightTex,bottomTex}, tangent, binormal);
+
+ normal.set(n1.add(n2).add(n3).add(n4).normalizeLocal());
+ }
+
+ private Vector3f calculateNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint) {
+ Vector3f normal = new Vector3f();
+ normal.set(firstPoint).subtractLocal(rootPoint)
+ .crossLocal(secondPoint.subtract(rootPoint)).normalizeLocal();
+ return normal;
+ }
+
+ protected Vector3f getMeshNormal(int x, int z) {
+ if (x >= size || z >= size)
+ return null; // out of range
+
+ int index = (z*size+x)*3;
+ FloatBuffer nb = (FloatBuffer)this.getMesh().getBuffer(Type.Normal).getData();
+ Vector3f normal = new Vector3f();
+ normal.x = nb.get(index);
+ normal.y = nb.get(index+1);
+ normal.z = nb.get(index+2);
+ return normal;
+ }
+
+ /**
+ * Locks the mesh (sets it static) to improve performance.
+ * But it it not editable then. Set unlock to make it editable.
+ */
+ public void lockMesh() {
+ getMesh().setStatic();
+ }
+
+ /**
+ * Unlocks the mesh (sets it dynamic) to make it editable.
+ * It will be editable but performance will be reduced.
+ * Call lockMesh to improve performance.
+ */
+ public void unlockMesh() {
+ getMesh().setDynamic();
+ }
+
+ /**
+ * Returns the offset amount this terrain patch uses for textures.
+ *
+ * @return The current offset amount.
+ */
+ public float getOffsetAmount() {
+ return offsetAmount;
+ }
+
+ /**
+ * Returns the step scale that stretches the height map.
+ *
+ * @return The current step scale.
+ */
+ public Vector3f getStepScale() {
+ return stepScale;
+ }
+
+ /**
+ * Returns the total size of the terrain.
+ *
+ * @return The terrain's total size.
+ */
+ public int getTotalSize() {
+ return totalSize;
+ }
+
+ /**
+ * Returns the size of this terrain patch.
+ *
+ * @return The current patch size.
+ */
+ public int getSize() {
+ return size;
+ }
+
+ /**
+ * Returns the current offset amount. This is used when building texture
+ * coordinates.
+ *
+ * @return The current offset amount.
+ */
+ public Vector2f getOffset() {
+ return offset;
+ }
+
+ /**
+ * Sets the value for the current offset amount to use when building texture
+ * coordinates. Note that this does <b>NOT </b> rebuild the terrain at all.
+ * This is mostly used for outside constructors of terrain patches.
+ *
+ * @param offset
+ * The new texture offset.
+ */
+ public void setOffset(Vector2f offset) {
+ this.offset = offset;
+ }
+
+ /**
+ * Sets the size of this terrain patch. Note that this does <b>NOT </b>
+ * rebuild the terrain at all. This is mostly used for outside constructors
+ * of terrain patches.
+ *
+ * @param size
+ * The new size.
+ */
+ public void setSize(int size) {
+ this.size = size;
+
+ maxLod = -1; // reset it
+ }
+
+ /**
+ * Sets the total size of the terrain . Note that this does <b>NOT </b>
+ * rebuild the terrain at all. This is mostly used for outside constructors
+ * of terrain patches.
+ *
+ * @param totalSize
+ * The new total size.
+ */
+ public void setTotalSize(int totalSize) {
+ this.totalSize = totalSize;
+ }
+
+ /**
+ * Sets the step scale of this terrain patch's height map. Note that this
+ * does <b>NOT </b> rebuild the terrain at all. This is mostly used for
+ * outside constructors of terrain patches.
+ *
+ * @param stepScale
+ * The new step scale.
+ */
+ public void setStepScale(Vector3f stepScale) {
+ this.stepScale = stepScale;
+ }
+
+ /**
+ * Sets the offset of this terrain texture map. Note that this does <b>NOT
+ * </b> rebuild the terrain at all. This is mostly used for outside
+ * constructors of terrain patches.
+ *
+ * @param offsetAmount
+ * The new texture offset.
+ */
+ public void setOffsetAmount(float offsetAmount) {
+ this.offsetAmount = offsetAmount;
+ }
+
+ /**
+ * @return Returns the quadrant.
+ */
+ public short getQuadrant() {
+ return quadrant;
+ }
+
+ /**
+ * @param quadrant
+ * The quadrant to set.
+ */
+ public void setQuadrant(short quadrant) {
+ this.quadrant = quadrant;
+ }
+
+ public int getLod() {
+ return lod;
+ }
+
+ public void setLod(int lod) {
+ this.lod = lod;
+ }
+
+ public int getPreviousLod() {
+ return previousLod;
+ }
+
+ public void setPreviousLod(int previousLod) {
+ this.previousLod = previousLod;
+ }
+
+ protected int getLodLeft() {
+ return lodLeft;
+ }
+
+ protected void setLodLeft(int lodLeft) {
+ this.lodLeft = lodLeft;
+ }
+
+ protected int getLodTop() {
+ return lodTop;
+ }
+
+ protected void setLodTop(int lodTop) {
+ this.lodTop = lodTop;
+ }
+
+ protected int getLodRight() {
+ return lodRight;
+ }
+
+ protected void setLodRight(int lodRight) {
+ this.lodRight = lodRight;
+ }
+
+ protected int getLodBottom() {
+ return lodBottom;
+ }
+
+ protected void setLodBottom(int lodBottom) {
+ this.lodBottom = lodBottom;
+ }
+
+ /*public void setLodCalculator(LodCalculatorFactory lodCalculatorFactory) {
+ this.lodCalculatorFactory = lodCalculatorFactory;
+ setLodCalculator(lodCalculatorFactory.createCalculator(this));
+ }*/
+
+ @Override
+ public int collideWith(Collidable other, CollisionResults results) throws UnsupportedCollisionException {
+ if (refreshFlags != 0)
+ throw new IllegalStateException("Scene graph must be updated" +
+ " before checking collision");
+
+ if (other instanceof BoundingVolume)
+ if (!getWorldBound().intersects((BoundingVolume)other))
+ return 0;
+
+ if(other instanceof Ray)
+ return collideWithRay((Ray)other, results);
+ else if (other instanceof BoundingVolume)
+ return collideWithBoundingVolume((BoundingVolume)other, results);
+ else {
+ throw new UnsupportedCollisionException("TerrainPatch cannnot collide with "+other.getClass().getName());
+ }
+ }
+
+
+ private int collideWithRay(Ray ray, CollisionResults results) {
+ // This should be handled in the root terrain quad
+ return 0;
+ }
+
+ private int collideWithBoundingVolume(BoundingVolume boundingVolume, CollisionResults results) {
+ if (boundingVolume instanceof BoundingBox)
+ return collideWithBoundingBox((BoundingBox)boundingVolume, results);
+ else if(boundingVolume instanceof BoundingSphere) {
+ BoundingSphere sphere = (BoundingSphere) boundingVolume;
+ BoundingBox bbox = new BoundingBox(boundingVolume.getCenter().clone(), sphere.getRadius(),
+ sphere.getRadius(),
+ sphere.getRadius());
+ return collideWithBoundingBox(bbox, results);
+ }
+ return 0;
+ }
+
+ protected Vector3f worldCoordinateToLocal(Vector3f loc) {
+ Vector3f translated = new Vector3f();
+ translated.x = loc.x/getWorldScale().x - getWorldTranslation().x;
+ translated.y = loc.y/getWorldScale().y - getWorldTranslation().y;
+ translated.z = loc.z/getWorldScale().z - getWorldTranslation().z;
+ return translated;
+ }
+
+ /**
+ * This most definitely is not optimized.
+ */
+ private int collideWithBoundingBox(BoundingBox bbox, CollisionResults results) {
+
+ // test the four corners, for cases where the bbox dimensions are less than the terrain grid size, which is probably most of the time
+ Vector3f topLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
+ Vector3f topRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
+ Vector3f bottomLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent()));
+ Vector3f bottomRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent()));
+
+ Triangle t = getTriangle(topLeft.x, topLeft.z);
+ if (t != null && bbox.collideWith(t, results) > 0)
+ return 1;
+ t = getTriangle(topRight.x, topRight.z);
+ if (t != null && bbox.collideWith(t, results) > 0)
+ return 1;
+ t = getTriangle(bottomLeft.x, bottomLeft.z);
+ if (t != null && bbox.collideWith(t, results) > 0)
+ return 1;
+ t = getTriangle(bottomRight.x, bottomRight.z);
+ if (t != null && bbox.collideWith(t, results) > 0)
+ return 1;
+
+ // box is larger than the points on the terrain, so test against the points
+ for (float z=topLeft.z; z<bottomLeft.z; z+=1) {
+ for (float x=topLeft.x; x<topRight.x; x+=1) {
+
+ if (x < 0 || z < 0 || x >= size || z >= size)
+ continue;
+ t = getTriangle(x,z);
+ if (t != null && bbox.collideWith(t, results) > 0)
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ // the mesh is removed, and reloaded when read() is called
+ // this reduces the save size to 10% by not saving the mesh
+ Mesh temp = getMesh();
+ mesh = null;
+
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(size, "size", 16);
+ oc.write(totalSize, "totalSize", 16);
+ oc.write(quadrant, "quadrant", (short)0);
+ oc.write(stepScale, "stepScale", Vector3f.UNIT_XYZ);
+ oc.write(offset, "offset", Vector3f.UNIT_XYZ);
+ oc.write(offsetAmount, "offsetAmount", 0);
+ //oc.write(lodCalculator, "lodCalculator", null);
+ //oc.write(lodCalculatorFactory, "lodCalculatorFactory", null);
+ oc.write(lodEntropy, "lodEntropy", null);
+ oc.write(geomap, "geomap", null);
+
+ setMesh(temp);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ size = ic.readInt("size", 16);
+ totalSize = ic.readInt("totalSize", 16);
+ quadrant = ic.readShort("quadrant", (short)0);
+ stepScale = (Vector3f) ic.readSavable("stepScale", Vector3f.UNIT_XYZ);
+ offset = (Vector2f) ic.readSavable("offset", Vector3f.UNIT_XYZ);
+ offsetAmount = ic.readFloat("offsetAmount", 0);
+ //lodCalculator = (LodCalculator) ic.readSavable("lodCalculator", new DistanceLodCalculator());
+ //lodCalculator.setTerrainPatch(this);
+ //lodCalculatorFactory = (LodCalculatorFactory) ic.readSavable("lodCalculatorFactory", null);
+ lodEntropy = ic.readFloatArray("lodEntropy", null);
+ geomap = (LODGeomap) ic.readSavable("geomap", null);
+
+ Mesh regen = geomap.createMesh(stepScale, new Vector2f(1,1), offset, offsetAmount, totalSize, false);
+ setMesh(regen);
+ //TangentBinormalGenerator.generate(this); // note that this will be removed
+ ensurePositiveVolumeBBox();
+ }
+
+ @Override
+ public TerrainPatch clone() {
+ TerrainPatch clone = new TerrainPatch();
+ clone.name = name.toString();
+ clone.size = size;
+ clone.totalSize = totalSize;
+ clone.quadrant = quadrant;
+ clone.stepScale = stepScale.clone();
+ clone.offset = offset.clone();
+ clone.offsetAmount = offsetAmount;
+ //clone.lodCalculator = lodCalculator.clone();
+ //clone.lodCalculator.setTerrainPatch(clone);
+ //clone.setLodCalculator(lodCalculatorFactory.clone());
+ clone.geomap = new LODGeomap(size, geomap.getHeightArray());
+ clone.setLocalTranslation(getLocalTranslation().clone());
+ Mesh m = clone.geomap.createMesh(clone.stepScale, Vector2f.UNIT_XY, clone.offset, clone.offsetAmount, clone.totalSize, false);
+ clone.setMesh(m);
+ clone.setMaterial(material.clone());
+ return clone;
+ }
+
+ protected void ensurePositiveVolumeBBox() {
+ if (getModelBound() instanceof BoundingBox) {
+ if (((BoundingBox)getModelBound()).getYExtent() < 0.001f) {
+ // a correction so the box always has a volume
+ ((BoundingBox)getModelBound()).setYExtent(0.001f);
+ updateWorldBound();
+ }
+ }
+ }
+
+
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java
new file mode 100644
index 0000000..0b9b218
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java
@@ -0,0 +1,1862 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bounding.BoundingVolume;
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResults;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.debug.WireBox;
+import com.jme3.terrain.ProgressMonitor;
+import com.jme3.terrain.Terrain;
+import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
+import com.jme3.terrain.geomipmap.picking.BresenhamTerrainPicker;
+import com.jme3.terrain.geomipmap.picking.TerrainPickData;
+import com.jme3.terrain.geomipmap.picking.TerrainPicker;
+import com.jme3.util.TangentBinormalGenerator;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A terrain quad is a node in the quad tree of the terrain system.
+ * The root terrain quad will be the only one that receives the update() call every frame
+ * and it will determine if there has been any LOD change.
+ *
+ * The leaves of the terrain quad tree are Terrain Patches. These have the real geometry mesh.
+ *
+ *
+ * Heightmap coordinates start from the bottom left of the world and work towards the
+ * top right.
+ *
+ * +x
+ * ^
+ * | ......N = length of heightmap
+ * | : :
+ * | : :
+ * | 0.....:
+ * +---------> +z
+ * (world coordinates)
+ *
+ * @author Brent Owens
+ */
+public class TerrainQuad extends Node implements Terrain {
+
+ protected Vector2f offset;
+
+ protected int totalSize; // the size of this entire terrain tree (on one side)
+
+ protected int size; // size of this quad, can be between totalSize and patchSize
+
+ protected int patchSize; // size of the individual patches
+
+ protected Vector3f stepScale;
+
+ protected float offsetAmount;
+
+ protected int quadrant = 0; // 1=upper left, 2=lower left, 3=upper right, 4=lower right
+
+ //protected LodCalculatorFactory lodCalculatorFactory;
+ //protected LodCalculator lodCalculator;
+
+ protected List<Vector3f> lastCameraLocations; // used for LOD calc
+ private boolean lodCalcRunning = false;
+ private int lodOffCount = 0;
+ private int maxLod = -1;
+ private HashMap<String,UpdatedTerrainPatch> updatedPatches;
+ private final Object updatePatchesLock = new Object();
+ private BoundingBox affectedAreaBBox; // only set in the root quad
+
+ private TerrainPicker picker;
+ private Vector3f lastScale = Vector3f.UNIT_XYZ;
+
+ protected ExecutorService executor;
+
+ protected ExecutorService createExecutorService() {
+ return Executors.newSingleThreadExecutor(new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread th = new Thread(r);
+ th.setName("jME Terrain Thread");
+ th.setDaemon(true);
+ return th;
+ }
+ });
+ }
+
+ public TerrainQuad() {
+ super("Terrain");
+ }
+
+ /**
+ *
+ * @param name the name of the scene element. This is required for
+ * identification and comparison purposes.
+ * @param patchSize size of the individual patches
+ * @param totalSize the size of this entire terrain tree (on one side)
+ * @param heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int totalSize, float[] heightMap) {
+ this(name, patchSize, totalSize, Vector3f.UNIT_XYZ, heightMap);
+ }
+
+ /**
+ *
+ * @param name the name of the scene element. This is required for
+ * identification and comparison purposes.
+ * @param patchSize size of the individual patches
+ * @param quadSize
+ * @param totalSize the size of this entire terrain tree (on one side)
+ * @param heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int quadSize, int totalSize, float[] heightMap) {
+ this(name, patchSize, totalSize, quadSize, Vector3f.UNIT_XYZ, heightMap);
+ }
+
+ /**
+ *
+ * @param name the name of the scene element. This is required for
+ * identification and comparison purposes.
+ * @param patchSize size of the individual patches
+ * @param size size of this quad, can be between totalSize and patchSize
+ * @param scale
+ * @param heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int size, Vector3f scale, float[] heightMap) {
+ this(name, patchSize, size, scale, heightMap, size, new Vector2f(), 0);
+ affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2);
+ fixNormalEdges(affectedAreaBBox);
+ addControl(new NormalRecalcControl(this));
+ }
+
+ /**
+ *
+ * @param name the name of the scene element. This is required for
+ * identification and comparison purposes.
+ * @param patchSize size of the individual patches
+ * @param totalSize the size of this entire terrain tree (on one side)
+ * @param quadSize
+ * @param scale
+ * @param heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int totalSize, int quadSize, Vector3f scale, float[] heightMap) {
+ this(name, patchSize, quadSize, scale, heightMap, totalSize, new Vector2f(), 0);
+ affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), totalSize*2, Float.MAX_VALUE, totalSize*2);
+ fixNormalEdges(affectedAreaBBox);
+ addControl(new NormalRecalcControl(this));
+ }
+
+ protected TerrainQuad(String name, int patchSize, int quadSize,
+ Vector3f scale, float[] heightMap, int totalSize,
+ Vector2f offset, float offsetAmount)
+ {
+ super(name);
+
+ if (heightMap == null)
+ heightMap = generateDefaultHeightMap(quadSize);
+
+ if (!FastMath.isPowerOfTwo(quadSize - 1)) {
+ throw new RuntimeException("size given: " + quadSize + " Terrain quad sizes may only be (2^N + 1)");
+ }
+ if (FastMath.sqrt(heightMap.length) > quadSize) {
+ Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Heightmap size is larger than the terrain size. Make sure your heightmap image is the same size as the terrain!");
+ }
+
+ this.offset = offset;
+ this.offsetAmount = offsetAmount;
+ this.totalSize = totalSize;
+ this.size = quadSize;
+ this.patchSize = patchSize;
+ this.stepScale = scale;
+ //this.lodCalculatorFactory = lodCalculatorFactory;
+ //this.lodCalculator = lodCalculator;
+ split(patchSize, heightMap);
+ }
+
+ /*public void setLodCalculatorFactory(LodCalculatorFactory lodCalculatorFactory) {
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).setLodCalculatorFactory(lodCalculatorFactory);
+ } else if (child instanceof TerrainPatch) {
+ ((TerrainPatch) child).setLodCalculator(lodCalculatorFactory.createCalculator((TerrainPatch) child));
+ }
+ }
+ }
+ }*/
+
+
+ /**
+ * Create just a flat heightmap
+ */
+ private float[] generateDefaultHeightMap(int size) {
+ float[] heightMap = new float[size*size];
+ return heightMap;
+ }
+
+ /**
+ * Call from the update() method of a terrain controller to update
+ * the LOD values of each patch.
+ * This will perform the geometry calculation in a background thread and
+ * do the actual update on the opengl thread.
+ */
+ public void update(List<Vector3f> locations, LodCalculator lodCalculator) {
+ updateLOD(locations, lodCalculator);
+ }
+
+ /**
+ * update the normals if there were any height changes recently.
+ * Should only be called on the root quad
+ */
+ protected void updateNormals() {
+
+ if (needToRecalculateNormals()) {
+ //TODO background-thread this if it ends up being expensive
+ fixNormals(affectedAreaBBox); // the affected patches
+ fixNormalEdges(affectedAreaBBox); // the edges between the patches
+
+ setNormalRecalcNeeded(null); // set to false
+ }
+ }
+
+ // do all of the LOD calculations
+ protected void updateLOD(List<Vector3f> locations, LodCalculator lodCalculator) {
+ // update any existing ones that need updating
+ updateQuadLODs();
+
+ if (lodCalculator.isLodOff()) {
+ // we want to calculate the base lod at least once
+ if (lodOffCount == 1)
+ return;
+ else
+ lodOffCount++;
+ } else
+ lodOffCount = 0;
+
+ if (lastCameraLocations != null) {
+ if (lastCameraLocationsTheSame(locations) && !lodCalculator.isLodOff())
+ return; // don't update if in same spot
+ else
+ lastCameraLocations = cloneVectorList(locations);
+ }
+ else {
+ lastCameraLocations = cloneVectorList(locations);
+ return;
+ }
+
+ if (isLodCalcRunning()) {
+ return;
+ }
+
+ if (getParent() instanceof TerrainQuad) {
+ return; // we just want the root quad to perform this.
+ }
+
+ if (executor == null)
+ executor = createExecutorService();
+
+ UpdateLOD updateLodThread = new UpdateLOD(locations, lodCalculator);
+ executor.execute(updateLodThread);
+ }
+
+ private synchronized boolean isLodCalcRunning() {
+ return lodCalcRunning;
+ }
+
+ private synchronized void setLodCalcRunning(boolean running) {
+ lodCalcRunning = running;
+ }
+
+ private List<Vector3f> cloneVectorList(List<Vector3f> locations) {
+ List<Vector3f> cloned = new ArrayList<Vector3f>();
+ for(Vector3f l : locations)
+ cloned.add(l.clone());
+ return cloned;
+ }
+
+ private boolean lastCameraLocationsTheSame(List<Vector3f> locations) {
+ boolean theSame = true;
+ for (Vector3f l : locations) {
+ for (Vector3f v : lastCameraLocations) {
+ if (!v.equals(l) ) {
+ theSame = false;
+ return false;
+ }
+ }
+ }
+ return theSame;
+ }
+
+ private int collideWithRay(Ray ray, CollisionResults results) {
+ if (picker == null)
+ picker = new BresenhamTerrainPicker(this);
+
+ Vector3f intersection = picker.getTerrainIntersection(ray, results);
+ if (intersection != null)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * Generate the entropy values for the terrain for the "perspective" LOD
+ * calculator. This routine can take a long time to run!
+ * @param progressMonitor optional
+ */
+ public void generateEntropy(ProgressMonitor progressMonitor) {
+ // only check this on the root quad
+ if (isRootQuad())
+ if (progressMonitor != null) {
+ int numCalc = (totalSize-1)/(patchSize-1); // make it an even number
+ progressMonitor.setMonitorMax(numCalc*numCalc);
+ }
+
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).generateEntropy(progressMonitor);
+ } else if (child instanceof TerrainPatch) {
+ ((TerrainPatch) child).generateLodEntropies();
+ if (progressMonitor != null)
+ progressMonitor.incrementProgress(1);
+ }
+ }
+ }
+
+ // only do this on the root quad
+ if (isRootQuad())
+ if (progressMonitor != null)
+ progressMonitor.progressComplete();
+ }
+
+ protected boolean isRootQuad() {
+ return (getParent() != null && !(getParent() instanceof TerrainQuad) );
+ }
+
+ public Material getMaterial() {
+ return getMaterial(null);
+ }
+
+ public Material getMaterial(Vector3f worldLocation) {
+ // get the material from one of the children. They all share the same material
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ return ((TerrainQuad)child).getMaterial(worldLocation);
+ } else if (child instanceof TerrainPatch) {
+ return ((TerrainPatch)child).getMaterial();
+ }
+ }
+ }
+ return null;
+ }
+
+ //public float getTextureCoordinateScale() {
+ // return 1f/(float)totalSize;
+ //}
+ public int getNumMajorSubdivisions() {
+ return 1;
+ }
+
+ /**
+ * Calculates the LOD of all child terrain patches.
+ */
+ private class UpdateLOD implements Runnable {
+ private List<Vector3f> camLocations;
+ private LodCalculator lodCalculator;
+
+ UpdateLOD(List<Vector3f> camLocations, LodCalculator lodCalculator) {
+ this.camLocations = camLocations;
+ this.lodCalculator = lodCalculator;
+ }
+
+ public void run() {
+ long start = System.currentTimeMillis();
+ if (isLodCalcRunning()) {
+ //System.out.println("thread already running");
+ return;
+ }
+ //System.out.println("spawned thread "+toString());
+ setLodCalcRunning(true);
+
+ // go through each patch and calculate its LOD based on camera distance
+ HashMap<String,UpdatedTerrainPatch> updated = new HashMap<String,UpdatedTerrainPatch>();
+ boolean lodChanged = calculateLod(camLocations, updated, lodCalculator); // 'updated' gets populated here
+
+ if (!lodChanged) {
+ // not worth updating anything else since no one's LOD changed
+ setLodCalcRunning(false);
+ return;
+ }
+ // then calculate its neighbour LOD values for seaming in the shader
+ findNeighboursLod(updated);
+
+ fixEdges(updated); // 'updated' can get added to here
+
+ reIndexPages(updated, lodCalculator.usesVariableLod());
+
+ setUpdateQuadLODs(updated); // set back to main ogl thread
+
+ setLodCalcRunning(false);
+ //double duration = (System.currentTimeMillis()-start);
+ //System.out.println("terminated in "+duration);
+ }
+ }
+
+ private void setUpdateQuadLODs(HashMap<String,UpdatedTerrainPatch> updated) {
+ synchronized (updatePatchesLock) {
+ updatedPatches = updated;
+ }
+ }
+
+ /**
+ * Back on the ogl thread: update the terrain patch geometries
+ * @param updatedPatches to be updated
+ */
+ private void updateQuadLODs() {
+ synchronized (updatePatchesLock) {
+
+ if (updatedPatches == null || updatedPatches.isEmpty())
+ return;
+
+ // do the actual geometry update here
+ for (UpdatedTerrainPatch utp : updatedPatches.values()) {
+ utp.updateAll();
+ }
+
+ updatedPatches.clear();
+ }
+ }
+
+ public boolean hasPatchesToUpdate() {
+ return updatedPatches != null && !updatedPatches.isEmpty();
+ }
+
+ protected boolean calculateLod(List<Vector3f> location, HashMap<String,UpdatedTerrainPatch> updates, LodCalculator lodCalculator) {
+
+ boolean lodChanged = false;
+
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ boolean b = ((TerrainQuad) child).calculateLod(location, updates, lodCalculator);
+ if (b)
+ lodChanged = true;
+ } else if (child instanceof TerrainPatch) {
+ boolean b = lodCalculator.calculateLod((TerrainPatch) child, location, updates);
+ if (b)
+ lodChanged = true;
+ }
+ }
+ }
+
+ return lodChanged;
+ }
+
+ protected synchronized void findNeighboursLod(HashMap<String,UpdatedTerrainPatch> updated) {
+ if (children != null) {
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).findNeighboursLod(updated);
+ } else if (child instanceof TerrainPatch) {
+
+ TerrainPatch patch = (TerrainPatch) child;
+ if (!patch.searchedForNeighboursAlready) {
+ // set the references to the neighbours
+ patch.rightNeighbour = findRightPatch(patch);
+ patch.bottomNeighbour = findDownPatch(patch);
+ patch.leftNeighbour = findLeftPatch(patch);
+ patch.topNeighbour = findTopPatch(patch);
+ patch.searchedForNeighboursAlready = true;
+ }
+ TerrainPatch right = patch.rightNeighbour;
+ TerrainPatch down = patch.bottomNeighbour;
+
+ UpdatedTerrainPatch utp = updated.get(patch.getName());
+ if (utp == null) {
+ utp = new UpdatedTerrainPatch(patch, patch.lod);
+ updated.put(utp.getName(), utp);
+ }
+
+ if (right != null) {
+ UpdatedTerrainPatch utpR = updated.get(right.getName());
+ if (utpR == null) {
+ utpR = new UpdatedTerrainPatch(right, right.lod);
+ updated.put(utpR.getName(), utpR);
+ }
+
+ utp.setRightLod(utpR.getNewLod());
+ utpR.setLeftLod(utp.getNewLod());
+ }
+ if (down != null) {
+ UpdatedTerrainPatch utpD = updated.get(down.getName());
+ if (utpD == null) {
+ utpD = new UpdatedTerrainPatch(down, down.lod);
+ updated.put(utpD.getName(), utpD);
+ }
+
+ utp.setBottomLod(utpD.getNewLod());
+ utpD.setTopLod(utp.getNewLod());
+ }
+
+ }
+ }
+ }
+ }
+
+ /**
+ * TerrainQuad caches neighbours for faster LOD checks.
+ * Sometimes you might want to reset this cache (for instance in TerrainGrid)
+ */
+ protected void resetCachedNeighbours() {
+ if (children != null) {
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).resetCachedNeighbours();
+ } else if (child instanceof TerrainPatch) {
+ TerrainPatch patch = (TerrainPatch) child;
+ patch.searchedForNeighboursAlready = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Find any neighbours that should have their edges seamed because another neighbour
+ * changed its LOD to a greater value (less detailed)
+ */
+ protected synchronized void fixEdges(HashMap<String,UpdatedTerrainPatch> updated) {
+ if (children != null) {
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).fixEdges(updated);
+ } else if (child instanceof TerrainPatch) {
+ TerrainPatch patch = (TerrainPatch) child;
+ UpdatedTerrainPatch utp = updated.get(patch.getName());
+
+ if(utp != null && utp.lodChanged()) {
+ if (!patch.searchedForNeighboursAlready) {
+ // set the references to the neighbours
+ patch.rightNeighbour = findRightPatch(patch);
+ patch.bottomNeighbour = findDownPatch(patch);
+ patch.leftNeighbour = findLeftPatch(patch);
+ patch.topNeighbour = findTopPatch(patch);
+ patch.searchedForNeighboursAlready = true;
+ }
+ TerrainPatch right = patch.rightNeighbour;
+ TerrainPatch down = patch.bottomNeighbour;
+ TerrainPatch top = patch.topNeighbour;
+ TerrainPatch left = patch.leftNeighbour;
+ if (right != null) {
+ UpdatedTerrainPatch utpR = updated.get(right.getName());
+ if (utpR == null) {
+ utpR = new UpdatedTerrainPatch(right, right.lod);
+ updated.put(utpR.getName(), utpR);
+ }
+ utpR.setFixEdges(true);
+ }
+ if (down != null) {
+ UpdatedTerrainPatch utpD = updated.get(down.getName());
+ if (utpD == null) {
+ utpD = new UpdatedTerrainPatch(down, down.lod);
+ updated.put(utpD.getName(), utpD);
+ }
+ utpD.setFixEdges(true);
+ }
+ if (top != null){
+ UpdatedTerrainPatch utpT = updated.get(top.getName());
+ if (utpT == null) {
+ utpT = new UpdatedTerrainPatch(top, top.lod);
+ updated.put(utpT.getName(), utpT);
+ }
+ utpT.setFixEdges(true);
+ }
+ if (left != null){
+ UpdatedTerrainPatch utpL = updated.get(left.getName());
+ if (utpL == null) {
+ utpL = new UpdatedTerrainPatch(left, left.lod);
+ updated.put(utpL.getName(), utpL);
+ }
+ utpL.setFixEdges(true);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected synchronized void reIndexPages(HashMap<String,UpdatedTerrainPatch> updated, boolean usesVariableLod) {
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).reIndexPages(updated, usesVariableLod);
+ } else if (child instanceof TerrainPatch) {
+ ((TerrainPatch) child).reIndexGeometry(updated, usesVariableLod);
+ }
+ }
+ }
+ }
+
+ /**
+ * <code>split</code> divides the heightmap data for four children. The
+ * children are either quads or patches. This is dependent on the size of the
+ * children. If the child's size is less than or equal to the set block
+ * size, then patches are created, otherwise, quads are created.
+ *
+ * @param blockSize
+ * the blocks size to test against.
+ * @param heightMap
+ * the height data.
+ */
+ protected void split(int blockSize, float[] heightMap) {
+ if ((size >> 1) + 1 <= blockSize) {
+ createQuadPatch(heightMap);
+ } else {
+ createQuad(blockSize, heightMap);
+ }
+
+ }
+
+ /**
+ * Quadrants, world coordinates, and heightmap coordinates (Y-up):
+ *
+ * -z
+ * -u |
+ * -v 1|3
+ * -x ----+---- x
+ * 2|4 u
+ * | v
+ * z
+ * <code>createQuad</code> generates four new quads from this quad.
+ * The heightmap's top left (0,0) coordinate is at the bottom, -x,-z
+ * coordinate of the terrain, so it grows in the positive x.z direction.
+ */
+ protected void createQuad(int blockSize, float[] heightMap) {
+ // create 4 terrain quads
+ int quarterSize = size >> 2;
+
+ int split = (size + 1) >> 1;
+
+ Vector2f tempOffset = new Vector2f();
+ offsetAmount += quarterSize;
+
+ //if (lodCalculator == null)
+ // lodCalculator = createDefaultLodCalculator(); // set a default one
+
+ // 1 upper left of heightmap, upper left quad
+ float[] heightBlock1 = createHeightSubBlock(heightMap, 0, 0, split);
+
+ Vector3f origin1 = new Vector3f(-quarterSize * stepScale.x, 0,
+ -quarterSize * stepScale.z);
+
+ tempOffset.x = offset.x;
+ tempOffset.y = offset.y;
+ tempOffset.x += origin1.x;
+ tempOffset.y += origin1.z;
+
+ TerrainQuad quad1 = new TerrainQuad(getName() + "Quad1", blockSize,
+ split, stepScale, heightBlock1, totalSize, tempOffset,
+ offsetAmount);
+ quad1.setLocalTranslation(origin1);
+ quad1.quadrant = 1;
+ this.attachChild(quad1);
+
+ // 2 lower left of heightmap, lower left quad
+ float[] heightBlock2 = createHeightSubBlock(heightMap, 0, split - 1,
+ split);
+
+ Vector3f origin2 = new Vector3f(-quarterSize * stepScale.x, 0,
+ quarterSize * stepScale.z);
+
+ tempOffset = new Vector2f();
+ tempOffset.x = offset.x;
+ tempOffset.y = offset.y;
+ tempOffset.x += origin2.x;
+ tempOffset.y += origin2.z;
+
+ TerrainQuad quad2 = new TerrainQuad(getName() + "Quad2", blockSize,
+ split, stepScale, heightBlock2, totalSize, tempOffset,
+ offsetAmount);
+ quad2.setLocalTranslation(origin2);
+ quad2.quadrant = 2;
+ this.attachChild(quad2);
+
+ // 3 upper right of heightmap, upper right quad
+ float[] heightBlock3 = createHeightSubBlock(heightMap, split - 1, 0,
+ split);
+
+ Vector3f origin3 = new Vector3f(quarterSize * stepScale.x, 0,
+ -quarterSize * stepScale.z);
+
+ tempOffset = new Vector2f();
+ tempOffset.x = offset.x;
+ tempOffset.y = offset.y;
+ tempOffset.x += origin3.x;
+ tempOffset.y += origin3.z;
+
+ TerrainQuad quad3 = new TerrainQuad(getName() + "Quad3", blockSize,
+ split, stepScale, heightBlock3, totalSize, tempOffset,
+ offsetAmount);
+ quad3.setLocalTranslation(origin3);
+ quad3.quadrant = 3;
+ this.attachChild(quad3);
+
+ // 4 lower right of heightmap, lower right quad
+ float[] heightBlock4 = createHeightSubBlock(heightMap, split - 1,
+ split - 1, split);
+
+ Vector3f origin4 = new Vector3f(quarterSize * stepScale.x, 0,
+ quarterSize * stepScale.z);
+
+ tempOffset = new Vector2f();
+ tempOffset.x = offset.x;
+ tempOffset.y = offset.y;
+ tempOffset.x += origin4.x;
+ tempOffset.y += origin4.z;
+
+ TerrainQuad quad4 = new TerrainQuad(getName() + "Quad4", blockSize,
+ split, stepScale, heightBlock4, totalSize, tempOffset,
+ offsetAmount);
+ quad4.setLocalTranslation(origin4);
+ quad4.quadrant = 4;
+ this.attachChild(quad4);
+
+ }
+
+ public void generateDebugTangents(Material mat) {
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad)child).generateDebugTangents(mat);
+ } else if (child instanceof TerrainPatch) {
+ Geometry debug = new Geometry( "Debug " + name,
+ TangentBinormalGenerator.genTbnLines( ((TerrainPatch)child).getMesh(), 0.8f));
+ attachChild(debug);
+ debug.setLocalTranslation(child.getLocalTranslation());
+ debug.setCullHint(CullHint.Never);
+ debug.setMaterial(mat);
+ }
+ }
+ }
+
+ /**
+ * <code>createQuadPatch</code> creates four child patches from this quad.
+ */
+ protected void createQuadPatch(float[] heightMap) {
+ // create 4 terrain patches
+ int quarterSize = size >> 2;
+ int halfSize = size >> 1;
+ int split = (size + 1) >> 1;
+
+ //if (lodCalculator == null)
+ // lodCalculator = createDefaultLodCalculator(); // set a default one
+
+ offsetAmount += quarterSize;
+
+ // 1 lower left
+ float[] heightBlock1 = createHeightSubBlock(heightMap, 0, 0, split);
+
+ Vector3f origin1 = new Vector3f(-halfSize * stepScale.x, 0, -halfSize
+ * stepScale.z);
+
+ Vector2f tempOffset1 = new Vector2f();
+ tempOffset1.x = offset.x;
+ tempOffset1.y = offset.y;
+ tempOffset1.x += origin1.x / 2;
+ tempOffset1.y += origin1.z / 2;
+
+ TerrainPatch patch1 = new TerrainPatch(getName() + "Patch1", split,
+ stepScale, heightBlock1, origin1, totalSize, tempOffset1,
+ offsetAmount);
+ patch1.setQuadrant((short) 1);
+ this.attachChild(patch1);
+ patch1.setModelBound(new BoundingBox());
+ patch1.updateModelBound();
+ //patch1.setLodCalculator(lodCalculator);
+ //TangentBinormalGenerator.generate(patch1);
+
+ // 2 upper left
+ float[] heightBlock2 = createHeightSubBlock(heightMap, 0, split - 1,
+ split);
+
+ Vector3f origin2 = new Vector3f(-halfSize * stepScale.x, 0, 0);
+
+ Vector2f tempOffset2 = new Vector2f();
+ tempOffset2.x = offset.x;
+ tempOffset2.y = offset.y;
+ tempOffset2.x += origin1.x / 2;
+ tempOffset2.y += quarterSize * stepScale.z;
+
+ TerrainPatch patch2 = new TerrainPatch(getName() + "Patch2", split,
+ stepScale, heightBlock2, origin2, totalSize, tempOffset2,
+ offsetAmount);
+ patch2.setQuadrant((short) 2);
+ this.attachChild(patch2);
+ patch2.setModelBound(new BoundingBox());
+ patch2.updateModelBound();
+ //patch2.setLodCalculator(lodCalculator);
+ //TangentBinormalGenerator.generate(patch2);
+
+ // 3 lower right
+ float[] heightBlock3 = createHeightSubBlock(heightMap, split - 1, 0,
+ split);
+
+ Vector3f origin3 = new Vector3f(0, 0, -halfSize * stepScale.z);
+
+ Vector2f tempOffset3 = new Vector2f();
+ tempOffset3.x = offset.x;
+ tempOffset3.y = offset.y;
+ tempOffset3.x += quarterSize * stepScale.x;
+ tempOffset3.y += origin3.z / 2;
+
+ TerrainPatch patch3 = new TerrainPatch(getName() + "Patch3", split,
+ stepScale, heightBlock3, origin3, totalSize, tempOffset3,
+ offsetAmount);
+ patch3.setQuadrant((short) 3);
+ this.attachChild(patch3);
+ patch3.setModelBound(new BoundingBox());
+ patch3.updateModelBound();
+ //patch3.setLodCalculator(lodCalculator);
+ //TangentBinormalGenerator.generate(patch3);
+
+ // 4 upper right
+ float[] heightBlock4 = createHeightSubBlock(heightMap, split - 1,
+ split - 1, split);
+
+ Vector3f origin4 = new Vector3f(0, 0, 0);
+
+ Vector2f tempOffset4 = new Vector2f();
+ tempOffset4.x = offset.x;
+ tempOffset4.y = offset.y;
+ tempOffset4.x += quarterSize * stepScale.x;
+ tempOffset4.y += quarterSize * stepScale.z;
+
+ TerrainPatch patch4 = new TerrainPatch(getName() + "Patch4", split,
+ stepScale, heightBlock4, origin4, totalSize, tempOffset4,
+ offsetAmount);
+ patch4.setQuadrant((short) 4);
+ this.attachChild(patch4);
+ patch4.setModelBound(new BoundingBox());
+ patch4.updateModelBound();
+ //patch4.setLodCalculator(lodCalculator);
+ //TangentBinormalGenerator.generate(patch4);
+ }
+
+ public float[] createHeightSubBlock(float[] heightMap, int x,
+ int y, int side) {
+ float[] rVal = new float[side * side];
+ int bsize = (int) FastMath.sqrt(heightMap.length);
+ int count = 0;
+ for (int i = y; i < side + y; i++) {
+ for (int j = x; j < side + x; j++) {
+ if (j < bsize && i < bsize)
+ rVal[count] = heightMap[j + (i * bsize)];
+ count++;
+ }
+ }
+ return rVal;
+ }
+
+ /**
+ * A handy method that will attach all bounding boxes of this terrain
+ * to the node you supply.
+ * Useful to visualize the bounding boxes when debugging.
+ *
+ * @param parent that will get the bounding box shapes of the terrain attached to
+ */
+ public void attachBoundChildren(Node parent) {
+ for (int i = 0; i < this.getQuantity(); i++) {
+ if (this.getChild(i) instanceof TerrainQuad) {
+ ((TerrainQuad) getChild(i)).attachBoundChildren(parent);
+ } else if (this.getChild(i) instanceof TerrainPatch) {
+ BoundingVolume bv = getChild(i).getWorldBound();
+ if (bv instanceof BoundingBox) {
+ attachBoundingBox((BoundingBox)bv, parent);
+ }
+ }
+ }
+ BoundingVolume bv = getWorldBound();
+ if (bv instanceof BoundingBox) {
+ attachBoundingBox((BoundingBox)bv, parent);
+ }
+ }
+
+ /**
+ * used by attachBoundChildren()
+ */
+ private void attachBoundingBox(BoundingBox bb, Node parent) {
+ WireBox wb = new WireBox(bb.getXExtent(), bb.getYExtent(), bb.getZExtent());
+ Geometry g = new Geometry();
+ g.setMesh(wb);
+ g.setLocalTranslation(bb.getCenter());
+ parent.attachChild(g);
+ }
+
+ /**
+ * Signal if the normal vectors for the terrain need to be recalculated.
+ * Does this by looking at the affectedAreaBBox bounding box. If the bbox
+ * exists already, then it will grow the box to fit the new changedPoint.
+ * If the affectedAreaBBox is null, then it will create one of unit size.
+ *
+ * @param needToRecalculateNormals if null, will cause needToRecalculateNormals() to return false
+ */
+ protected void setNormalRecalcNeeded(Vector2f changedPoint) {
+ if (changedPoint == null) { // set needToRecalculateNormals() to false
+ affectedAreaBBox = null;
+ return;
+ }
+
+ if (affectedAreaBBox == null) {
+ affectedAreaBBox = new BoundingBox(new Vector3f(changedPoint.x, 0, changedPoint.y), 1f, Float.MAX_VALUE, 1f); // unit length
+ } else {
+ // adjust size of box to be larger
+ affectedAreaBBox.mergeLocal(new BoundingBox(new Vector3f(changedPoint.x, 0, changedPoint.y), 1f, Float.MAX_VALUE, 1f));
+ }
+ }
+
+ protected boolean needToRecalculateNormals() {
+ if (affectedAreaBBox != null)
+ return true;
+ if (!lastScale.equals(getWorldScale())) {
+ affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size, Float.MAX_VALUE, size);
+ lastScale = getWorldScale();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This will cause all normals for this terrain quad to be recalculated
+ */
+ protected void setNeedToRecalculateNormals() {
+ affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2);
+ }
+
+ public float getHeightmapHeight(Vector2f xz) {
+ // offset
+ int halfSize = totalSize / 2;
+ int x = Math.round((xz.x / getWorldScale().x) + halfSize);
+ int z = Math.round((xz.y / getWorldScale().z) + halfSize);
+
+ return getHeightmapHeight(x, z);
+ }
+
+ /**
+ * This will just get the heightmap value at the supplied point,
+ * not an interpolated (actual) height value.
+ */
+ protected float getHeightmapHeight(int x, int z) {
+ int quad = findQuadrant(x, z);
+ int split = (size + 1) >> 1;
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial spat = children.get(i);
+ int col = x;
+ int row = z;
+ boolean match = false;
+
+ // get the childs quadrant
+ int childQuadrant = 0;
+ if (spat instanceof TerrainQuad) {
+ childQuadrant = ((TerrainQuad) spat).getQuadrant();
+ } else if (spat instanceof TerrainPatch) {
+ childQuadrant = ((TerrainPatch) spat).getQuadrant();
+ }
+
+ if (childQuadrant == 1 && (quad & 1) != 0) {
+ match = true;
+ } else if (childQuadrant == 2 && (quad & 2) != 0) {
+ row = z - split + 1;
+ match = true;
+ } else if (childQuadrant == 3 && (quad & 4) != 0) {
+ col = x - split + 1;
+ match = true;
+ } else if (childQuadrant == 4 && (quad & 8) != 0) {
+ col = x - split + 1;
+ row = z - split + 1;
+ match = true;
+ }
+
+ if (match) {
+ if (spat instanceof TerrainQuad) {
+ return ((TerrainQuad) spat).getHeightmapHeight(col, row);
+ } else if (spat instanceof TerrainPatch) {
+ return ((TerrainPatch) spat).getHeightmapHeight(col, row);
+ }
+ }
+
+ }
+ }
+ return Float.NaN;
+ }
+
+ protected Vector3f getMeshNormal(int x, int z) {
+ int quad = findQuadrant(x, z);
+ int split = (size + 1) >> 1;
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial spat = children.get(i);
+ int col = x;
+ int row = z;
+ boolean match = false;
+
+ // get the childs quadrant
+ int childQuadrant = 0;
+ if (spat instanceof TerrainQuad) {
+ childQuadrant = ((TerrainQuad) spat).getQuadrant();
+ } else if (spat instanceof TerrainPatch) {
+ childQuadrant = ((TerrainPatch) spat).getQuadrant();
+ }
+
+ if (childQuadrant == 1 && (quad & 1) != 0) {
+ match = true;
+ } else if (childQuadrant == 2 && (quad & 2) != 0) {
+ row = z - split + 1;
+ match = true;
+ } else if (childQuadrant == 3 && (quad & 4) != 0) {
+ col = x - split + 1;
+ match = true;
+ } else if (childQuadrant == 4 && (quad & 8) != 0) {
+ col = x - split + 1;
+ row = z - split + 1;
+ match = true;
+ }
+
+ if (match) {
+ if (spat instanceof TerrainQuad) {
+ return ((TerrainQuad) spat).getMeshNormal(col, row);
+ } else if (spat instanceof TerrainPatch) {
+ return ((TerrainPatch) spat).getMeshNormal(col, row);
+ }
+ }
+
+ }
+ }
+ return null;
+ }
+
+ public float getHeight(Vector2f xz) {
+ // offset
+ float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)totalSize / 2f);
+ float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)totalSize / 2f);
+ float height = getHeight(x, z);
+ height *= getWorldScale().y;
+ return height;
+ }
+
+ /*
+ * gets an interpolated value at the specified point
+ * @param x coordinate translated into actual (positive) terrain grid coordinates
+ * @param y coordinate translated into actual (positive) terrain grid coordinates
+ */
+ protected float getHeight(float x, float z) {
+ x-=0.5f;
+ z-=0.5f;
+ float col = FastMath.floor(x);
+ float row = FastMath.floor(z);
+ boolean onX = false;
+ if(1 - (x - col)-(z - row) < 0) // what triangle to interpolate on
+ onX = true;
+ // v1--v2 ^
+ // | / | |
+ // | / | |
+ // v3--v4 | Z
+ // |
+ // <-------Y
+ // X
+ float v1 = getHeightmapHeight((int) FastMath.ceil(x), (int) FastMath.ceil(z));
+ float v2 = getHeightmapHeight((int) FastMath.floor(x), (int) FastMath.ceil(z));
+ float v3 = getHeightmapHeight((int) FastMath.ceil(x), (int) FastMath.floor(z));
+ float v4 = getHeightmapHeight((int) FastMath.floor(x), (int) FastMath.floor(z));
+ if (onX) {
+ return ((x - col) + (z - row) - 1f)*v1 + (1f - (x - col))*v2 + (1f - (z - row))*v3;
+ } else {
+ return (1f - (x - col) - (z - row))*v4 + (z - row)*v2 + (x - col)*v3;
+ }
+ }
+
+ public Vector3f getNormal(Vector2f xz) {
+ // offset
+ float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)totalSize / 2f);
+ float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)totalSize / 2f);
+ Vector3f normal = getNormal(x, z, xz);
+
+ return normal;
+ }
+
+ protected Vector3f getNormal(float x, float z, Vector2f xz) {
+ x-=0.5f;
+ z-=0.5f;
+ float col = FastMath.floor(x);
+ float row = FastMath.floor(z);
+ boolean onX = false;
+ if(1 - (x - col)-(z - row) < 0) // what triangle to interpolate on
+ onX = true;
+ // v1--v2 ^
+ // | / | |
+ // | / | |
+ // v3--v4 | Z
+ // |
+ // <-------Y
+ // X
+ Vector3f n1 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.ceil(z));
+ Vector3f n2 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.ceil(z));
+ Vector3f n3 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.floor(z));
+ Vector3f n4 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.floor(z));
+
+ return n1.add(n2).add(n3).add(n4).normalize();
+ }
+
+ public void setHeight(Vector2f xz, float height) {
+ List<Vector2f> coord = new ArrayList<Vector2f>();
+ coord.add(xz);
+ List<Float> h = new ArrayList<Float>();
+ h.add(height);
+
+ setHeight(coord, h);
+ }
+
+ public void adjustHeight(Vector2f xz, float delta) {
+ List<Vector2f> coord = new ArrayList<Vector2f>();
+ coord.add(xz);
+ List<Float> h = new ArrayList<Float>();
+ h.add(delta);
+
+ adjustHeight(coord, h);
+ }
+
+ public void setHeight(List<Vector2f> xz, List<Float> height) {
+ setHeight(xz, height, true);
+ }
+
+ public void adjustHeight(List<Vector2f> xz, List<Float> height) {
+ setHeight(xz, height, false);
+ }
+
+ protected void setHeight(List<Vector2f> xz, List<Float> height, boolean overrideHeight) {
+ if (xz.size() != height.size())
+ throw new IllegalArgumentException("Both lists must be the same length!");
+
+ int halfSize = totalSize / 2;
+
+ List<LocationHeight> locations = new ArrayList<LocationHeight>();
+
+ // offset
+ for (int i=0; i<xz.size(); i++) {
+ int x = Math.round((xz.get(i).x / getWorldScale().x) + halfSize);
+ int z = Math.round((xz.get(i).y / getWorldScale().z) + halfSize);
+ locations.add(new LocationHeight(x,z,height.get(i)));
+ }
+
+ setHeight(locations, overrideHeight); // adjust height of the actual mesh
+
+ // signal that the normals need updating
+ for (int i=0; i<xz.size(); i++)
+ setNormalRecalcNeeded(xz.get(i) );
+ }
+
+ protected class LocationHeight {
+ int x;
+ int z;
+ float h;
+
+ LocationHeight(){}
+
+ LocationHeight(int x, int z, float h){
+ this.x = x;
+ this.z = z;
+ this.h = h;
+ }
+ }
+
+ protected void setHeight(List<LocationHeight> locations, boolean overrideHeight) {
+ if (children == null)
+ return;
+
+ List<LocationHeight> quadLH1 = new ArrayList<LocationHeight>();
+ List<LocationHeight> quadLH2 = new ArrayList<LocationHeight>();
+ List<LocationHeight> quadLH3 = new ArrayList<LocationHeight>();
+ List<LocationHeight> quadLH4 = new ArrayList<LocationHeight>();
+ Spatial quad1 = null;
+ Spatial quad2 = null;
+ Spatial quad3 = null;
+ Spatial quad4 = null;
+
+ // get the child quadrants
+ for (int i = children.size(); --i >= 0;) {
+ Spatial spat = children.get(i);
+ int childQuadrant = 0;
+ if (spat instanceof TerrainQuad) {
+ childQuadrant = ((TerrainQuad) spat).getQuadrant();
+ } else if (spat instanceof TerrainPatch) {
+ childQuadrant = ((TerrainPatch) spat).getQuadrant();
+ }
+
+ if (childQuadrant == 1)
+ quad1 = spat;
+ else if (childQuadrant == 2)
+ quad2 = spat;
+ else if (childQuadrant == 3)
+ quad3 = spat;
+ else if (childQuadrant == 4)
+ quad4 = spat;
+ }
+
+ int split = (size + 1) >> 1;
+
+ // distribute each locationHeight into the quadrant it intersects
+ for (LocationHeight lh : locations) {
+ int quad = findQuadrant(lh.x, lh.z);
+
+ int col = lh.x;
+ int row = lh.z;
+
+ if ((quad & 1) != 0) {
+ quadLH1.add(lh);
+ }
+ if ((quad & 2) != 0) {
+ row = lh.z - split + 1;
+ quadLH2.add(new LocationHeight(lh.x, row, lh.h));
+ }
+ if ((quad & 4) != 0) {
+ col = lh.x - split + 1;
+ quadLH3.add(new LocationHeight(col, lh.z, lh.h));
+ }
+ if ((quad & 8) != 0) {
+ col = lh.x - split + 1;
+ row = lh.z - split + 1;
+ quadLH4.add(new LocationHeight(col, row, lh.h));
+ }
+ }
+
+ // send the locations to the children
+ if (!quadLH1.isEmpty()) {
+ if (quad1 instanceof TerrainQuad)
+ ((TerrainQuad)quad1).setHeight(quadLH1, overrideHeight);
+ else if(quad1 instanceof TerrainPatch)
+ ((TerrainPatch)quad1).setHeight(quadLH1, overrideHeight);
+ }
+
+ if (!quadLH2.isEmpty()) {
+ if (quad2 instanceof TerrainQuad)
+ ((TerrainQuad)quad2).setHeight(quadLH2, overrideHeight);
+ else if(quad2 instanceof TerrainPatch)
+ ((TerrainPatch)quad2).setHeight(quadLH2, overrideHeight);
+ }
+
+ if (!quadLH3.isEmpty()) {
+ if (quad3 instanceof TerrainQuad)
+ ((TerrainQuad)quad3).setHeight(quadLH3, overrideHeight);
+ else if(quad3 instanceof TerrainPatch)
+ ((TerrainPatch)quad3).setHeight(quadLH3, overrideHeight);
+ }
+
+ if (!quadLH4.isEmpty()) {
+ if (quad4 instanceof TerrainQuad)
+ ((TerrainQuad)quad4).setHeight(quadLH4, overrideHeight);
+ else if(quad4 instanceof TerrainPatch)
+ ((TerrainPatch)quad4).setHeight(quadLH4, overrideHeight);
+ }
+ }
+
+ protected boolean isPointOnTerrain(int x, int z) {
+ return (x >= 0 && x <= totalSize && z >= 0 && z <= totalSize);
+ }
+
+
+ public int getTerrainSize() {
+ return totalSize;
+ }
+
+
+ // a position can be in multiple quadrants, so use a bit anded value.
+ private int findQuadrant(int x, int y) {
+ int split = (size + 1) >> 1;
+ int quads = 0;
+ if (x < split && y < split)
+ quads |= 1;
+ if (x < split && y >= split - 1)
+ quads |= 2;
+ if (x >= split - 1 && y < split)
+ quads |= 4;
+ if (x >= split - 1 && y >= split - 1)
+ quads |= 8;
+ return quads;
+ }
+
+ /**
+ * lock or unlock the meshes of this terrain.
+ * Locked meshes are uneditable but have better performance.
+ * @param locked or unlocked
+ */
+ public void setLocked(boolean locked) {
+ for (int i = 0; i < this.getQuantity(); i++) {
+ if (this.getChild(i) instanceof TerrainQuad) {
+ ((TerrainQuad) getChild(i)).setLocked(locked);
+ } else if (this.getChild(i) instanceof TerrainPatch) {
+ if (locked)
+ ((TerrainPatch) getChild(i)).lockMesh();
+ else
+ ((TerrainPatch) getChild(i)).unlockMesh();
+ }
+ }
+ }
+
+
+ public int getQuadrant() {
+ return quadrant;
+ }
+
+ public void setQuadrant(short quadrant) {
+ this.quadrant = quadrant;
+ }
+
+
+ protected TerrainPatch getPatch(int quad) {
+ if (children != null)
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainPatch) {
+ TerrainPatch tb = (TerrainPatch) child;
+ if (tb.getQuadrant() == quad)
+ return tb;
+ }
+ }
+ return null;
+ }
+
+ protected TerrainQuad getQuad(int quad) {
+ if (children != null)
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ TerrainQuad tq = (TerrainQuad) child;
+ if (tq.getQuadrant() == quad)
+ return tq;
+ }
+ }
+ return null;
+ }
+
+ protected TerrainPatch findRightPatch(TerrainPatch tp) {
+ if (tp.getQuadrant() == 1)
+ return getPatch(3);
+ else if (tp.getQuadrant() == 2)
+ return getPatch(4);
+ else if (tp.getQuadrant() == 3) {
+ // find the patch to the right and ask it for child 1.
+ TerrainQuad quad = findRightQuad();
+ if (quad != null)
+ return quad.getPatch(1);
+ } else if (tp.getQuadrant() == 4) {
+ // find the patch to the right and ask it for child 2.
+ TerrainQuad quad = findRightQuad();
+ if (quad != null)
+ return quad.getPatch(2);
+ }
+
+ return null;
+ }
+
+ protected TerrainPatch findDownPatch(TerrainPatch tp) {
+ if (tp.getQuadrant() == 1)
+ return getPatch(2);
+ else if (tp.getQuadrant() == 3)
+ return getPatch(4);
+ else if (tp.getQuadrant() == 2) {
+ // find the patch below and ask it for child 1.
+ TerrainQuad quad = findDownQuad();
+ if (quad != null)
+ return quad.getPatch(1);
+ } else if (tp.getQuadrant() == 4) {
+ TerrainQuad quad = findDownQuad();
+ if (quad != null)
+ return quad.getPatch(3);
+ }
+
+ return null;
+ }
+
+
+ protected TerrainPatch findTopPatch(TerrainPatch tp) {
+ if (tp.getQuadrant() == 2)
+ return getPatch(1);
+ else if (tp.getQuadrant() == 4)
+ return getPatch(3);
+ else if (tp.getQuadrant() == 1) {
+ // find the patch above and ask it for child 2.
+ TerrainQuad quad = findTopQuad();
+ if (quad != null)
+ return quad.getPatch(2);
+ } else if (tp.getQuadrant() == 3) {
+ TerrainQuad quad = findTopQuad();
+ if (quad != null)
+ return quad.getPatch(4);
+ }
+
+ return null;
+ }
+
+ protected TerrainPatch findLeftPatch(TerrainPatch tp) {
+ if (tp.getQuadrant() == 3)
+ return getPatch(1);
+ else if (tp.getQuadrant() == 4)
+ return getPatch(2);
+ else if (tp.getQuadrant() == 1) {
+ // find the patch above and ask it for child 2.
+ TerrainQuad quad = findLeftQuad();
+ if (quad != null)
+ return quad.getPatch(3);
+ } else if (tp.getQuadrant() == 2) {
+ TerrainQuad quad = findLeftQuad();
+ if (quad != null)
+ return quad.getPatch(4);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findRightQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad pQuad = (TerrainQuad) getParent();
+
+ if (quadrant == 1)
+ return pQuad.getQuad(3);
+ else if (quadrant == 2)
+ return pQuad.getQuad(4);
+ else if (quadrant == 3) {
+ TerrainQuad quad = pQuad.findRightQuad();
+ if (quad != null)
+ return quad.getQuad(1);
+ } else if (quadrant == 4) {
+ TerrainQuad quad = pQuad.findRightQuad();
+ if (quad != null)
+ return quad.getQuad(2);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findDownQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad pQuad = (TerrainQuad) getParent();
+
+ if (quadrant == 1)
+ return pQuad.getQuad(2);
+ else if (quadrant == 3)
+ return pQuad.getQuad(4);
+ else if (quadrant == 2) {
+ TerrainQuad quad = pQuad.findDownQuad();
+ if (quad != null)
+ return quad.getQuad(1);
+ } else if (quadrant == 4) {
+ TerrainQuad quad = pQuad.findDownQuad();
+ if (quad != null)
+ return quad.getQuad(3);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findTopQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad pQuad = (TerrainQuad) getParent();
+
+ if (quadrant == 2)
+ return pQuad.getQuad(1);
+ else if (quadrant == 4)
+ return pQuad.getQuad(3);
+ else if (quadrant == 1) {
+ TerrainQuad quad = pQuad.findTopQuad();
+ if (quad != null)
+ return quad.getQuad(2);
+ } else if (quadrant == 3) {
+ TerrainQuad quad = pQuad.findTopQuad();
+ if (quad != null)
+ return quad.getQuad(4);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findLeftQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad pQuad = (TerrainQuad) getParent();
+
+ if (quadrant == 3)
+ return pQuad.getQuad(1);
+ else if (quadrant == 4)
+ return pQuad.getQuad(2);
+ else if (quadrant == 1) {
+ TerrainQuad quad = pQuad.findLeftQuad();
+ if (quad != null)
+ return quad.getQuad(3);
+ } else if (quadrant == 2) {
+ TerrainQuad quad = pQuad.findLeftQuad();
+ if (quad != null)
+ return quad.getQuad(4);
+ }
+
+ return null;
+ }
+
+ /**
+ * Find what terrain patches need normal recalculations and update
+ * their normals;
+ */
+ protected void fixNormals(BoundingBox affectedArea) {
+ if (children == null)
+ return;
+
+ // go through the children and see if they collide with the affectedAreaBBox
+ // if they do, then update their normals
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ if (affectedArea != null && affectedArea.intersects(((TerrainQuad) child).getWorldBound()) )
+ ((TerrainQuad) child).fixNormals(affectedArea);
+ } else if (child instanceof TerrainPatch) {
+ if (affectedArea != null && affectedArea.intersects(((TerrainPatch) child).getWorldBound()) )
+ ((TerrainPatch) child).updateNormals(); // recalculate the patch's normals
+ }
+ }
+ }
+
+ /**
+ * fix the normals on the edge of the terrain patches.
+ */
+ protected void fixNormalEdges(BoundingBox affectedArea) {
+ if (children == null)
+ return;
+
+ for (int x = children.size(); --x >= 0;) {
+ Spatial child = children.get(x);
+ if (child instanceof TerrainQuad) {
+ if (affectedArea != null && affectedArea.intersects(((TerrainQuad) child).getWorldBound()) )
+ ((TerrainQuad) child).fixNormalEdges(affectedArea);
+ } else if (child instanceof TerrainPatch) {
+ if (affectedArea != null && !affectedArea.intersects(((TerrainPatch) child).getWorldBound()) ) // if doesn't intersect, continue
+ continue;
+
+ TerrainPatch tp = (TerrainPatch) child;
+ TerrainPatch right = findRightPatch(tp);
+ TerrainPatch bottom = findDownPatch(tp);
+ TerrainPatch top = findTopPatch(tp);
+ TerrainPatch left = findLeftPatch(tp);
+ TerrainPatch topLeft = null;
+ if (top != null)
+ topLeft = findLeftPatch(top);
+ TerrainPatch bottomRight = null;
+ if (right != null)
+ bottomRight = findDownPatch(right);
+ TerrainPatch topRight = null;
+ if (top != null)
+ topRight = findRightPatch(top);
+ TerrainPatch bottomLeft = null;
+ if (left != null)
+ bottomLeft = findDownPatch(left);
+
+ tp.fixNormalEdges(right, bottom, top, left, bottomRight, bottomLeft, topRight, topLeft);
+
+ }
+ } // for each child
+
+ }
+
+
+
+ @Override
+ public int collideWith(Collidable other, CollisionResults results){
+ int total = 0;
+
+ if (other instanceof Ray)
+ return collideWithRay((Ray)other, results);
+
+ // if it didn't collide with this bbox, return
+ if (other instanceof BoundingVolume)
+ if (!this.getWorldBound().intersects((BoundingVolume)other))
+ return total;
+
+ for (Spatial child : children){
+ total += child.collideWith(other, results);
+ }
+ return total;
+ }
+
+ /**
+ * Gather the terrain patches that intersect the given ray (toTest).
+ * This only tests the bounding boxes
+ * @param toTest
+ * @param results
+ */
+ public void findPick(Ray toTest, List<TerrainPickData> results) {
+
+ if (getWorldBound() != null) {
+ if (getWorldBound().intersects(toTest)) {
+ // further checking needed.
+ for (int i = 0; i < getQuantity(); i++) {
+ if (children.get(i) instanceof TerrainPatch) {
+ TerrainPatch tp = (TerrainPatch) children.get(i);
+ tp.ensurePositiveVolumeBBox();
+ if (tp.getWorldBound().intersects(toTest)) {
+ CollisionResults cr = new CollisionResults();
+ toTest.collideWith(tp.getWorldBound(), cr);
+ if (cr != null && cr.getClosestCollision() != null) {
+ cr.getClosestCollision().getDistance();
+ results.add(new TerrainPickData(tp, cr.getClosestCollision()));
+ }
+ }
+ }
+ else
+ ((TerrainQuad) children.get(i)).findPick(toTest, results);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Retrieve all Terrain Patches from all children and store them
+ * in the 'holder' list
+ * @param holder must not be null, will be populated when returns
+ */
+ public void getAllTerrainPatches(List<TerrainPatch> holder) {
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).getAllTerrainPatches(holder);
+ } else if (child instanceof TerrainPatch) {
+ holder.add((TerrainPatch)child);
+ }
+ }
+ }
+ }
+
+ public void getAllTerrainPatchesWithTranslation(Map<TerrainPatch,Vector3f> holder, Vector3f translation) {
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).getAllTerrainPatchesWithTranslation(holder, translation.clone().add(child.getLocalTranslation()));
+ } else if (child instanceof TerrainPatch) {
+ //if (holder.size() < 4)
+ holder.put((TerrainPatch)child, translation.clone().add(child.getLocalTranslation()));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void read(JmeImporter e) throws IOException {
+ super.read(e);
+ InputCapsule c = e.getCapsule(this);
+ size = c.readInt("size", 0);
+ stepScale = (Vector3f) c.readSavable("stepScale", null);
+ offset = (Vector2f) c.readSavable("offset", new Vector2f(0,0));
+ offsetAmount = c.readFloat("offsetAmount", 0);
+ quadrant = c.readInt("quadrant", 0);
+ totalSize = c.readInt("totalSize", 0);
+ //lodCalculator = (LodCalculator) c.readSavable("lodCalculator", createDefaultLodCalculator());
+ //lodCalculatorFactory = (LodCalculatorFactory) c.readSavable("lodCalculatorFactory", null);
+
+ if ( !(getParent() instanceof TerrainQuad) ) {
+ BoundingBox all = new BoundingBox(getWorldTranslation(), totalSize, totalSize, totalSize);
+ affectedAreaBBox = all;
+ updateNormals();
+ }
+ }
+
+ @Override
+ public void write(JmeExporter e) throws IOException {
+ super.write(e);
+ OutputCapsule c = e.getCapsule(this);
+ c.write(size, "size", 0);
+ c.write(totalSize, "totalSize", 0);
+ c.write(stepScale, "stepScale", null);
+ c.write(offset, "offset", new Vector2f(0,0));
+ c.write(offsetAmount, "offsetAmount", 0);
+ c.write(quadrant, "quadrant", 0);
+ //c.write(lodCalculatorFactory, "lodCalculatorFactory", null);
+ //c.write(lodCalculator, "lodCalculator", null);
+ }
+
+ @Override
+ public TerrainQuad clone() {
+ return this.clone(true);
+ }
+
+ @Override
+ public TerrainQuad clone(boolean cloneMaterials) {
+ TerrainQuad quadClone = (TerrainQuad) super.clone(cloneMaterials);
+ quadClone.name = name.toString();
+ quadClone.size = size;
+ quadClone.totalSize = totalSize;
+ if (stepScale != null) {
+ quadClone.stepScale = stepScale.clone();
+ }
+ if (offset != null) {
+ quadClone.offset = offset.clone();
+ }
+ quadClone.offsetAmount = offsetAmount;
+ quadClone.quadrant = quadrant;
+ //quadClone.lodCalculatorFactory = lodCalculatorFactory.clone();
+ //quadClone.lodCalculator = lodCalculator.clone();
+
+ TerrainLodControl lodControlCloned = this.getControl(TerrainLodControl.class);
+ TerrainLodControl lodControl = quadClone.getControl(TerrainLodControl.class);
+
+ if (lodControlCloned != null && !(getParent() instanceof TerrainQuad)) {
+ //lodControlCloned.setLodCalculator(lodControl.getLodCalculator().clone());
+ }
+ NormalRecalcControl normalControl = getControl(NormalRecalcControl.class);
+ if (normalControl != null)
+ normalControl.setTerrain(this);
+
+ return quadClone;
+ }
+
+ public int getMaxLod() {
+ if (maxLod < 0)
+ maxLod = Math.max(1, (int) (FastMath.log(size-1)/FastMath.log(2)) -1); // -1 forces our minimum of 4 triangles wide
+
+ return maxLod;
+ }
+
+ public int getPatchSize() {
+ return patchSize;
+ }
+
+ public int getTotalSize() {
+ return totalSize;
+ }
+
+ public float[] getHeightMap() {
+
+ float[] hm = null;
+ int length = ((size-1)/2)+1;
+ int area = size*size;
+ hm = new float[area];
+
+ if (getChildren() != null && !getChildren().isEmpty()) {
+ float[] ul=null, ur=null, bl=null, br=null;
+ // get the child heightmaps
+ if (getChild(0) instanceof TerrainPatch) {
+ for (Spatial s : getChildren()) {
+ if ( ((TerrainPatch)s).getQuadrant() == 1)
+ ul = ((TerrainPatch)s).getHeightMap();
+ else if(((TerrainPatch) s).getQuadrant() == 2)
+ bl = ((TerrainPatch)s).getHeightMap();
+ else if(((TerrainPatch) s).getQuadrant() == 3)
+ ur = ((TerrainPatch)s).getHeightMap();
+ else if(((TerrainPatch) s).getQuadrant() == 4)
+ br = ((TerrainPatch)s).getHeightMap();
+ }
+ }
+ else {
+ ul = getQuad(1).getHeightMap();
+ bl = getQuad(2).getHeightMap();
+ ur = getQuad(3).getHeightMap();
+ br = getQuad(4).getHeightMap();
+ }
+
+ // combine them into a single heightmap
+
+
+ // first upper blocks
+ for (int y=0; y<length; y++) { // rows
+ for (int x1=0; x1<length; x1++) {
+ int row = y*size;
+ hm[row+x1] = ul[y*length+x1];
+ }
+ for (int x2=1; x2<length; x2++) {
+ int row = y*size + length;
+ hm[row+x2-1] = ur[y*length + x2];
+ }
+ }
+ // second lower blocks
+ int rowOffset = size*length;
+ for (int y=1; y<length; y++) { // rows
+ for (int x1=0; x1<length; x1++) {
+ int row = (y-1)*size;
+ hm[rowOffset+row+x1] = bl[y*length+x1];
+ }
+ for (int x2=1; x2<length; x2++) {
+ int row = (y-1)*size + length;
+ hm[rowOffset+row+x2-1] = br[y*length + x2];
+ }
+ }
+ }
+
+ return hm;
+ }
+}
+
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/UpdatedTerrainPatch.java b/engine/src/terrain/com/jme3/terrain/geomipmap/UpdatedTerrainPatch.java
new file mode 100644
index 0000000..7a335d0
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/UpdatedTerrainPatch.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap;
+
+import com.jme3.scene.VertexBuffer.Type;
+import java.nio.IntBuffer;
+
+/**
+ * Stores a terrain patch's details so the LOD background thread can update
+ * the actual terrain patch back on the ogl thread.
+ *
+ * @author Brent Owens
+ *
+ */
+public class UpdatedTerrainPatch {
+
+ private TerrainPatch updatedPatch;
+ private int newLod;
+ private int previousLod;
+ private int rightLod,topLod,leftLod,bottomLod;
+ private IntBuffer newIndexBuffer;
+ private boolean reIndexNeeded = false;
+ private boolean fixEdges = false;
+
+ public UpdatedTerrainPatch(TerrainPatch updatedPatch, int newLod) {
+ this.updatedPatch = updatedPatch;
+ this.newLod = newLod;
+ }
+
+ public UpdatedTerrainPatch(TerrainPatch updatedPatch, int newLod, int prevLOD, boolean reIndexNeeded) {
+ this.updatedPatch = updatedPatch;
+ this.newLod = newLod;
+ this.previousLod = prevLOD;
+ this.reIndexNeeded = reIndexNeeded;
+ if (this.newLod <= 0)
+ throw new IllegalArgumentException();
+ }
+
+ public String getName() {
+ return updatedPatch.getName();
+ }
+
+ protected boolean lodChanged() {
+ if (reIndexNeeded && previousLod != newLod)
+ return true;
+ else
+ return false;
+ }
+
+ protected TerrainPatch getUpdatedPatch() {
+ return updatedPatch;
+ }
+
+ protected void setUpdatedPatch(TerrainPatch updatedPatch) {
+ this.updatedPatch = updatedPatch;
+ }
+
+ protected int getNewLod() {
+ return newLod;
+ }
+
+ public void setNewLod(int newLod) {
+ this.newLod = newLod;
+ if (this.newLod < 0)
+ throw new IllegalArgumentException();
+ }
+
+ protected IntBuffer getNewIndexBuffer() {
+ return newIndexBuffer;
+ }
+
+ protected void setNewIndexBuffer(IntBuffer newIndexBuffer) {
+ this.newIndexBuffer = newIndexBuffer;
+ }
+
+
+ protected int getRightLod() {
+ return rightLod;
+ }
+
+
+ protected void setRightLod(int rightLod) {
+ this.rightLod = rightLod;
+ }
+
+
+ protected int getTopLod() {
+ return topLod;
+ }
+
+
+ protected void setTopLod(int topLod) {
+ this.topLod = topLod;
+ }
+
+
+ protected int getLeftLod() {
+ return leftLod;
+ }
+
+
+ protected void setLeftLod(int leftLod) {
+ this.leftLod = leftLod;
+ }
+
+
+ protected int getBottomLod() {
+ return bottomLod;
+ }
+
+
+ protected void setBottomLod(int bottomLod) {
+ this.bottomLod = bottomLod;
+ }
+
+ public boolean isReIndexNeeded() {
+ return reIndexNeeded;
+ }
+
+ public void setReIndexNeeded(boolean reIndexNeeded) {
+ this.reIndexNeeded = reIndexNeeded;
+ }
+
+ public boolean isFixEdges() {
+ return fixEdges;
+ }
+
+ public void setFixEdges(boolean fixEdges) {
+ this.fixEdges = fixEdges;
+ }
+
+ public int getPreviousLod() {
+ return previousLod;
+ }
+
+ public void setPreviousLod(int previousLod) {
+ this.previousLod = previousLod;
+ }
+
+ public void updateAll() {
+ updatedPatch.setLod(newLod);
+ updatedPatch.setLodRight(rightLod);
+ updatedPatch.setLodTop(topLod);
+ updatedPatch.setLodLeft(leftLod);
+ updatedPatch.setLodBottom(bottomLod);
+ if (newIndexBuffer != null && (reIndexNeeded || fixEdges)) {
+ updatedPatch.setPreviousLod(previousLod);
+ updatedPatch.getMesh().clearBuffer(Type.Index);
+ updatedPatch.getMesh().setBuffer(Type.Index, 3, newIndexBuffer);
+ }
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/grid/AssetTileLoader.java b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/AssetTileLoader.java
new file mode 100644
index 0000000..0c61e75
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/AssetTileLoader.java
@@ -0,0 +1,92 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.geomipmap.grid;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainGridTileLoader;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class AssetTileLoader implements TerrainGridTileLoader {
+
+ private AssetManager manager;
+ private String assetPath;
+ private String name;
+ private int size;
+ private int patchSize;
+ private int quadSize;
+
+ public AssetTileLoader() {
+ }
+
+ public AssetTileLoader(AssetManager manager, String name, String assetPath) {
+ this.manager = manager;
+ this.name = name;
+ this.assetPath = assetPath;
+ }
+
+ public TerrainQuad getTerrainQuadAt(Vector3f location) {
+ String modelName = assetPath + "/" + name + "_" + Math.round(location.x) + "_" + Math.round(location.y) + "_" + Math.round(location.z) + ".j3o";
+ Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Load terrain grid tile: {0}", modelName);
+ TerrainQuad quad = null;
+ try {
+ quad = (TerrainQuad) manager.loadModel(modelName);
+ } catch (Exception e) {
+// e.printStackTrace();
+ }
+ if (quad == null) {
+ Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Could not load terrain grid tile: {0}", modelName);
+ quad = createNewQuad(location);
+ } else {
+ Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Loaded terrain grid tile: {0}", modelName);
+ }
+ return quad;
+ }
+
+ public String getAssetPath() {
+ return assetPath;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setPatchSize(int patchSize) {
+ this.patchSize = patchSize;
+ }
+
+ public void setQuadSize(int quadSize) {
+ this.quadSize = quadSize;
+ }
+
+ private TerrainQuad createNewQuad(Vector3f location) {
+ TerrainQuad q = new TerrainQuad("Quad" + location, patchSize, quadSize, null);
+ return q;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule c = ex.getCapsule(this);
+ c.write(assetPath, "assetPath", null);
+ c.write(name, "name", null);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule c = im.getCapsule(this);
+ manager = im.getAssetManager();
+ assetPath = c.readString("assetPath", null);
+ name = c.readString("name", null);
+ }
+} \ No newline at end of file
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/grid/FractalTileLoader.java b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/FractalTileLoader.java
new file mode 100644
index 0000000..9eaa9a2
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/FractalTileLoader.java
@@ -0,0 +1,87 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.geomipmap.grid;
+
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainGridTileLoader;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.heightmap.AbstractHeightMap;
+import com.jme3.terrain.heightmap.HeightMap;
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import com.jme3.terrain.noise.Basis;
+
+/**
+ *
+ * @author Anthyon, normenhansen
+ */
+public class FractalTileLoader implements TerrainGridTileLoader{
+
+ public class FloatBufferHeightMap extends AbstractHeightMap {
+
+ private final FloatBuffer buffer;
+
+ public FloatBufferHeightMap(FloatBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ @Override
+ public boolean load() {
+ this.heightData = this.buffer.array();
+ return true;
+ }
+
+ }
+
+ private int patchSize;
+ private int quadSize;
+ private final Basis base;
+ private final float heightScale;
+
+ public FractalTileLoader(Basis base, float heightScale) {
+ this.base = base;
+ this.heightScale = heightScale;
+ }
+
+ private HeightMap getHeightMapAt(Vector3f location) {
+ AbstractHeightMap heightmap = null;
+
+ FloatBuffer buffer = this.base.getBuffer(location.x * (this.quadSize - 1), location.z * (this.quadSize - 1), 0, this.quadSize);
+
+ float[] arr = buffer.array();
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = arr[i] * this.heightScale;
+ }
+ heightmap = new FloatBufferHeightMap(buffer);
+ heightmap.load();
+ return heightmap;
+ }
+
+ public TerrainQuad getTerrainQuadAt(Vector3f location) {
+ HeightMap heightMapAt = getHeightMapAt(location);
+ TerrainQuad q = new TerrainQuad("Quad" + location, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
+ return q;
+ }
+
+ public void setPatchSize(int patchSize) {
+ this.patchSize = patchSize;
+ }
+
+ public void setQuadSize(int quadSize) {
+ this.quadSize = quadSize;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ //TODO: serialization
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ //TODO: serialization
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/grid/ImageTileLoader.java b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/ImageTileLoader.java
new file mode 100644
index 0000000..cef6a72
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/grid/ImageTileLoader.java
@@ -0,0 +1,153 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.geomipmap.grid;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.AssetNotFoundException;
+import com.jme3.asset.TextureKey;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainGridTileLoader;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.heightmap.*;
+import com.jme3.texture.Texture;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ *
+ * @author Anthyon, normenhansen
+ */
+public class ImageTileLoader implements TerrainGridTileLoader{
+ private static final Logger logger = Logger.getLogger(ImageTileLoader.class.getName());
+ private final AssetManager assetManager;
+ private final Namer namer;
+ private int patchSize;
+ private int quadSize;
+ private float heightScale = 1;
+ //private int imageType = BufferedImage.TYPE_USHORT_GRAY; // 16 bit grayscale
+ //private ImageHeightmap customImageHeightmap;
+
+ public ImageTileLoader(final String textureBase, final String textureExt, AssetManager assetManager) {
+ this(assetManager, new Namer() {
+
+ public String getName(int x, int y) {
+ return textureBase + "_" + x + "_" + y + "." + textureExt;
+ }
+ });
+ }
+
+ public ImageTileLoader(AssetManager assetManager, Namer namer) {
+ this.assetManager = assetManager;
+ this.namer = namer;
+ }
+
+ /**
+ * Effects vertical scale of the height of the terrain when loaded.
+ */
+ public void setHeightScale(float heightScale) {
+ this.heightScale = heightScale;
+ }
+
+
+ /**
+ * Lets you specify the type of images that are being loaded. All images
+ * must be the same type.
+ * @param imageType eg. BufferedImage.TYPE_USHORT_GRAY
+ */
+ /*public void setImageType(int imageType) {
+ this.imageType = imageType;
+ }*/
+
+ /**
+ * The ImageHeightmap that will parse the image type that you
+ * specify with setImageType().
+ * @param customImageHeightmap must extend AbstractHeightmap
+ */
+ /*public void setCustomImageHeightmap(ImageHeightmap customImageHeightmap) {
+ if (!(customImageHeightmap instanceof AbstractHeightMap)) {
+ throw new IllegalArgumentException("customImageHeightmap must be an AbstractHeightMap!");
+ }
+ this.customImageHeightmap = customImageHeightmap;
+ }*/
+
+ private HeightMap getHeightMapAt(Vector3f location) {
+ // HEIGHTMAP image (for the terrain heightmap)
+ int x = (int) location.x;
+ int z = (int) location.z;
+
+ AbstractHeightMap heightmap = null;
+ //BufferedImage im = null;
+
+ String name = null;
+ try {
+ name = namer.getName(x, z);
+ logger.log(Level.INFO, "Loading heightmap from file: {0}", name);
+ final Texture texture = assetManager.loadTexture(new TextureKey(name));
+ heightmap = new ImageBasedHeightMap(texture.getImage());
+ /*if (assetInfo != null){
+ InputStream in = assetInfo.openStream();
+ im = ImageIO.read(in);
+ } else {
+ im = new BufferedImage(patchSize, patchSize, imageType);
+ logger.log(Level.WARNING, "File: {0} not found, loading zero heightmap instead", name);
+ }*/
+ // CREATE HEIGHTMAP
+ /*if (imageType == BufferedImage.TYPE_USHORT_GRAY) {
+ heightmap = new Grayscale16BitHeightMap(im);
+ } else if (imageType == BufferedImage.TYPE_3BYTE_BGR) {
+ heightmap = new ImageBasedHeightMap(im);
+ } else if (customImageHeightmap != null && customImageHeightmap instanceof AbstractHeightMap) {
+ // If it gets here, it means you have specified a different image type, and you must
+ // then also supply a custom image heightmap class that can parse that image into
+ // a heightmap.
+ customImageHeightmap.setImage(im);
+ heightmap = (AbstractHeightMap) customImageHeightmap;
+ } else {
+ // error, no supported image format and no custom image heightmap specified
+ if (customImageHeightmap == null)
+ logger.log(Level.SEVERE, "Custom image type specified [{0}] but no customImageHeightmap declared! Use setCustomImageHeightmap()",imageType);
+ if (!(customImageHeightmap instanceof AbstractHeightMap))
+ logger.severe("customImageHeightmap must be an AbstractHeightMap!");
+ return null;
+ }*/
+ heightmap.setHeightScale(1);
+ heightmap.load();
+ //} catch (IOException e) {
+ // e.printStackTrace();
+ } catch (AssetNotFoundException e) {
+ logger.log(Level.WARNING, "Asset {0} not found, loading zero heightmap instead", name);
+ }
+ return heightmap;
+ }
+
+ public void setSize(int size) {
+ this.patchSize = size - 1;
+ }
+
+ public TerrainQuad getTerrainQuadAt(Vector3f location) {
+ HeightMap heightMapAt = getHeightMapAt(location);
+ TerrainQuad q = new TerrainQuad("Quad" + location, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
+ return q;
+ }
+
+ public void setPatchSize(int patchSize) {
+ this.patchSize = patchSize;
+ }
+
+ public void setQuadSize(int quadSize) {
+ this.quadSize = quadSize;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ //TODO: serialization
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ //TODO: serialization
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/DistanceLodCalculator.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/DistanceLodCalculator.java
new file mode 100644
index 0000000..3ab34c9
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/DistanceLodCalculator.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import com.jme3.terrain.geomipmap.UpdatedTerrainPatch;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Calculates the LOD of the terrain based on its distance from the
+ * cameras. Taking the minimum distance from all cameras.
+ *
+ * @author bowens
+ */
+public class DistanceLodCalculator implements LodCalculator {
+
+ private int size; // size of a terrain patch
+ private float lodMultiplier = 2;
+ private boolean turnOffLod = false;
+
+ public DistanceLodCalculator() {
+ }
+
+ public DistanceLodCalculator(int patchSize, float multiplier) {
+ this.size = patchSize;
+ this.lodMultiplier = multiplier;
+ }
+
+ public boolean calculateLod(TerrainPatch terrainPatch, List<Vector3f> locations, HashMap<String, UpdatedTerrainPatch> updates) {
+ float distance = getCenterLocation(terrainPatch).distance(locations.get(0));
+
+
+ if (turnOffLod) {
+ // set to full detail
+ int prevLOD = terrainPatch.getLod();
+ UpdatedTerrainPatch utp = updates.get(terrainPatch.getName());
+ if (utp == null) {
+ utp = new UpdatedTerrainPatch(terrainPatch, 0);
+ updates.put(utp.getName(), utp);
+ }
+ utp.setNewLod(0);
+ utp.setPreviousLod(prevLOD);
+ utp.setReIndexNeeded(true);
+ return true;
+ }
+
+ // go through each lod level to find the one we are in
+ for (int i = 0; i <= terrainPatch.getMaxLod(); i++) {
+ if (distance < getLodDistanceThreshold() * (i + 1)*terrainPatch.getWorldScale().x || i == terrainPatch.getMaxLod()) {
+ boolean reIndexNeeded = false;
+ if (i != terrainPatch.getLod()) {
+ reIndexNeeded = true;
+ //System.out.println("lod change: "+lod+" > "+i+" dist: "+distance);
+ }
+ int prevLOD = terrainPatch.getLod();
+ //previousLod = lod;
+ //lod = i;
+ UpdatedTerrainPatch utp = updates.get(terrainPatch.getName());
+ if (utp == null) {
+ utp = new UpdatedTerrainPatch(terrainPatch, i);//save in here, do not update actual variables
+ updates.put(utp.getName(), utp);
+ }
+ utp.setPreviousLod(prevLOD);
+ utp.setReIndexNeeded(reIndexNeeded);
+
+ return reIndexNeeded;
+ }
+ }
+
+ return false;
+ }
+
+ protected Vector3f getCenterLocation(TerrainPatch terrainPatch) {
+ Vector3f loc = terrainPatch.getWorldTranslation().clone();
+ loc.x += terrainPatch.getSize()*terrainPatch.getWorldScale().x / 2;
+ loc.z += terrainPatch.getSize()*terrainPatch.getWorldScale().z / 2;
+ return loc;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(size, "patchSize", 32);
+ oc.write(lodMultiplier, "lodMultiplier", 32);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule ic = im.getCapsule(this);
+ size = ic.readInt("patchSize", 32);
+ lodMultiplier = ic.readFloat("lodMultiplier", 2.7f);
+ }
+
+ @Override
+ public LodCalculator clone() {
+ DistanceLodCalculator clone = new DistanceLodCalculator(size, lodMultiplier);
+ return clone;
+ }
+
+ @Override
+ public String toString() {
+ return "DistanceLodCalculator "+size+"*"+lodMultiplier;
+ }
+
+ /**
+ * Gets the camera distance where the LOD level will change
+ */
+ protected float getLodDistanceThreshold() {
+ return size*lodMultiplier;
+ }
+
+ /**
+ * Does this calculator require the terrain to have the difference of
+ * LOD levels of neighbours to be more than 1.
+ */
+ public boolean usesVariableLod() {
+ return false;
+ }
+
+ public float getLodMultiplier() {
+ return lodMultiplier;
+ }
+
+ public void setLodMultiplier(float lodMultiplier) {
+ this.lodMultiplier = lodMultiplier;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public void setSize(int size) {
+ this.size = size;
+ }
+
+ public void turnOffLod() {
+ turnOffLod = true;
+ }
+
+ public boolean isLodOff() {
+ return turnOffLod;
+ }
+
+ public void turnOnLod() {
+ turnOffLod = false;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculator.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculator.java
new file mode 100644
index 0000000..2d6c364
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculator.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import com.jme3.terrain.geomipmap.UpdatedTerrainPatch;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Calculate the Level of Detail of a terrain patch based on the
+ * cameras, or other locations.
+ *
+ * @author Brent Owens
+ */
+public interface LodCalculator extends Savable, Cloneable {
+
+ public boolean calculateLod(TerrainPatch terrainPatch, List<Vector3f> locations, HashMap<String,UpdatedTerrainPatch> updates);
+
+ public LodCalculator clone();
+
+ public void turnOffLod();
+ public void turnOnLod();
+ public boolean isLodOff();
+
+ /**
+ * If true, then this calculator can cause neighbouring terrain chunks to
+ * have LOD levels that are greater than 1 apart.
+ * Entropy algorithms will want to return true for this. Straight distance
+ * calculations will just want to return false.
+ */
+ public boolean usesVariableLod();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculatorFactory.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculatorFactory.java
new file mode 100644
index 0000000..6d5f5b8
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodCalculatorFactory.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.Savable;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+
+/**
+ * Creates LOD Calculator objects for the terrain patches.
+ *
+ * @author Brent Owens
+ * @deprecated phasing this out
+ */
+public interface LodCalculatorFactory extends Savable, Cloneable {
+
+ public LodCalculator createCalculator();
+ public LodCalculator createCalculator(TerrainPatch terrainPatch);
+
+ public LodCalculatorFactory clone();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodDistanceCalculatorFactory.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodDistanceCalculatorFactory.java
new file mode 100644
index 0000000..29d5c25
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodDistanceCalculatorFactory.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import java.io.IOException;
+
+/**
+ *
+ * @author bowens
+ * @deprecated phasing out
+ */
+public class LodDistanceCalculatorFactory implements LodCalculatorFactory {
+
+ private float lodThresholdSize = 2.7f;
+ private LodThreshold lodThreshold = null;
+
+
+ public LodDistanceCalculatorFactory() {
+ }
+
+ public LodDistanceCalculatorFactory(LodThreshold lodThreshold) {
+ this.lodThreshold = lodThreshold;
+ }
+
+ public LodCalculator createCalculator() {
+ return new DistanceLodCalculator();
+ }
+
+ public LodCalculator createCalculator(TerrainPatch terrainPatch) {
+ return new DistanceLodCalculator();
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule c = ex.getCapsule(this);
+ c.write(lodThreshold, "lodThreshold", null);
+ c.write(lodThresholdSize, "lodThresholdSize", 2);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule c = im.getCapsule(this);
+ lodThresholdSize = c.readFloat("lodThresholdSize", 2);
+ lodThreshold = (LodThreshold) c.readSavable("lodThreshold", null);
+ }
+
+ @Override
+ public LodDistanceCalculatorFactory clone() {
+ LodDistanceCalculatorFactory clone = new LodDistanceCalculatorFactory();
+ clone.lodThreshold = lodThreshold.clone();
+ clone.lodThresholdSize = lodThresholdSize;
+ return clone;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodPerspectiveCalculatorFactory.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodPerspectiveCalculatorFactory.java
new file mode 100644
index 0000000..b453e26
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodPerspectiveCalculatorFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.renderer.Camera;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import java.io.IOException;
+
+/**
+ * TODO: Make it work with multiple cameras
+ * TODO: Fix the cracks when the lod differences are greater than 1
+ * for two adjacent blocks.
+ * @deprecated phasing out
+ */
+public class LodPerspectiveCalculatorFactory implements LodCalculatorFactory {
+
+ private Camera cam;
+ private float pixelError;
+
+ public LodPerspectiveCalculatorFactory(Camera cam, float pixelError){
+ this.cam = cam;
+ this.pixelError = pixelError;
+ }
+
+ public LodCalculator createCalculator() {
+ return new PerspectiveLodCalculator(cam, pixelError);
+ }
+
+ public LodCalculator createCalculator(TerrainPatch terrainPatch) {
+ PerspectiveLodCalculator p = new PerspectiveLodCalculator(cam, pixelError);
+ return p;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ }
+
+ @Override
+ public LodCalculatorFactory clone() {
+ try {
+ return (LodCalculatorFactory) super.clone();
+ } catch (CloneNotSupportedException ex) {
+ throw new AssertionError();
+ }
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodThreshold.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodThreshold.java
new file mode 100644
index 0000000..73d5c35
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/LodThreshold.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.Savable;
+
+
+/**
+ * Calculates the LOD value based on where the camera is.
+ * This is plugged into the Terrain system and any terrain
+ * using LOD will use this to determine when a patch of the
+ * terrain should switch Levels of Detail.
+ *
+ * @author bowens
+ */
+public interface LodThreshold extends Savable, Cloneable {
+
+ /**
+ * A distance of how far between each LOD threshold.
+ */
+ public float getLodDistanceThreshold();
+
+ public LodThreshold clone();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/PerspectiveLodCalculator.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/PerspectiveLodCalculator.java
new file mode 100644
index 0000000..312b17d
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/PerspectiveLodCalculator.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import com.jme3.terrain.geomipmap.UpdatedTerrainPatch;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+
+public class PerspectiveLodCalculator implements LodCalculator {
+
+ private TerrainPatch patch;
+ private Camera cam;
+ private float[] entropyDistances;
+ private float pixelError;
+
+ public PerspectiveLodCalculator() {}
+
+ public PerspectiveLodCalculator(Camera cam, float pixelError){
+ this.cam = cam;
+ this.pixelError = pixelError;
+ }
+
+ /**
+ * This computes the "C" value in the geomipmapping paper.
+ * See section "2.3.1.2 Pre-calculating d"
+ *
+ * @param cam
+ * @param pixelLimit
+ * @return
+ */
+ private float getCameraConstant(Camera cam, float pixelLimit){
+ float n = cam.getFrustumNear();
+ float t = FastMath.abs(cam.getFrustumTop());
+ float A = n / t;
+ float v_res = cam.getHeight();
+ float T = (2f * pixelLimit) / v_res;
+ return A / T;
+ }
+
+ public boolean calculateLod(List<Vector3f> locations, HashMap<String, UpdatedTerrainPatch> updates) {
+ return calculateLod(patch, locations, updates);
+ }
+
+ public boolean calculateLod(TerrainPatch terrainPatch, List<Vector3f> locations, HashMap<String, UpdatedTerrainPatch> updates) {
+ if (entropyDistances == null){
+ // compute entropy distances
+ float[] lodEntropies = patch.getLodEntropies();
+ entropyDistances = new float[lodEntropies.length];
+ float cameraConstant = getCameraConstant(cam, pixelError);
+ for (int i = 0; i < lodEntropies.length; i++){
+ entropyDistances[i] = lodEntropies[i] * cameraConstant;
+ }
+ }
+
+ Vector3f patchPos = getCenterLocation(patch);
+
+ // vector from camera to patch
+ //Vector3f toPatchDir = locations.get(0).subtract(patchPos).normalizeLocal();
+ //float facing = cam.getDirection().dot(toPatchDir);
+ float distance = patchPos.distance(locations.get(0));
+
+ // go through each lod level to find the one we are in
+ for (int i = 0; i <= patch.getMaxLod(); i++) {
+ if (distance < entropyDistances[i] || i == patch.getMaxLod()){
+ boolean reIndexNeeded = false;
+ if (i != patch.getLod()) {
+ reIndexNeeded = true;
+// System.out.println("lod change: "+lod+" > "+i+" dist: "+distance);
+ }
+ int prevLOD = patch.getLod();
+
+ //previousLod = lod;
+ //lod = i;
+ UpdatedTerrainPatch utp = updates.get(patch.getName());
+ if (utp == null) {
+ utp = new UpdatedTerrainPatch(patch, i);//save in here, do not update actual variables
+ updates.put(utp.getName(), utp);
+ }
+ utp.setPreviousLod(prevLOD);
+ utp.setReIndexNeeded(reIndexNeeded);
+ return reIndexNeeded;
+ }
+ }
+
+ return false;
+ }
+
+ public Vector3f getCenterLocation(TerrainPatch patch) {
+ Vector3f loc = patch.getWorldTranslation().clone();
+ loc.x += patch.getSize() / 2;
+ loc.z += patch.getSize() / 2;
+ return loc;
+ }
+
+ @Override
+ public LodCalculator clone() {
+ try {
+ return (LodCalculator) super.clone();
+ } catch (CloneNotSupportedException ex) {
+ throw new AssertionError();
+ }
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ }
+
+ public boolean usesVariableLod() {
+ return true;
+ }
+
+ public float getPixelError() {
+ return pixelError;
+ }
+
+ public void setPixelError(float pixelError) {
+ this.pixelError = pixelError;
+ }
+
+ public void setCam(Camera cam) {
+ this.cam = cam;
+ }
+
+ public void turnOffLod() {
+ //TODO
+ }
+
+ public boolean isLodOff() {
+ return false; //TODO
+ }
+
+ public void turnOnLod() {
+ //TODO
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/SimpleLodThreshold.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/SimpleLodThreshold.java
new file mode 100644
index 0000000..bd512e5
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/SimpleLodThreshold.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.lodcalc;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.terrain.Terrain;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import java.io.IOException;
+
+
+/**
+ * Just multiplies the terrain patch size by 2. So every two
+ * patches away the camera is, the LOD changes.
+ *
+ * Set it higher to have the LOD change less frequently.
+ *
+ * @author bowens
+ */
+public class SimpleLodThreshold implements LodThreshold {
+
+ private int size; // size of a terrain patch
+ private float lodMultiplier = 2;
+
+
+ public SimpleLodThreshold() {
+ }
+
+ public SimpleLodThreshold(Terrain terrain) {
+ if (terrain instanceof TerrainQuad)
+ this.size = ((TerrainQuad)terrain).getPatchSize();
+ }
+
+ public SimpleLodThreshold(int patchSize, float lodMultiplier) {
+ this.size = patchSize;
+ }
+
+ public float getLodMultiplier() {
+ return lodMultiplier;
+ }
+
+ public void setLodMultiplier(float lodMultiplier) {
+ this.lodMultiplier = lodMultiplier;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public void setSize(int size) {
+ this.size = size;
+ }
+
+
+ public float getLodDistanceThreshold() {
+ return size*lodMultiplier;
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(size, "size", 16);
+ oc.write(lodMultiplier, "lodMultiplier", 2);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule ic = im.getCapsule(this);
+ size = ic.readInt("size", 16);
+ lodMultiplier = ic.readInt("lodMultiplier", 2);
+ }
+
+ @Override
+ public LodThreshold clone() {
+ SimpleLodThreshold clone = new SimpleLodThreshold();
+ clone.size = size;
+ clone.lodMultiplier = lodMultiplier;
+
+ return clone;
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleLodThreshold "+size+", "+lodMultiplier;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/util/EntropyComputeUtil.java b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/util/EntropyComputeUtil.java
new file mode 100644
index 0000000..c358aae
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/lodcalc/util/EntropyComputeUtil.java
@@ -0,0 +1,75 @@
+package com.jme3.terrain.geomipmap.lodcalc.util;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+/**
+ * Computes the entropy value δ (delta) for a given terrain block and
+ * LOD level.
+ * See the geomipmapping paper section
+ * "2.3.1 Choosing the appropriate GeoMipMap level"
+ *
+ * @author Kirill Vainer
+ */
+public class EntropyComputeUtil {
+
+ public static float computeLodEntropy(Mesh terrainBlock, IntBuffer lodIndices){
+ // Bounding box for the terrain block
+ BoundingBox bbox = (BoundingBox) terrainBlock.getBound();
+
+ // Vertex positions for the block
+ FloatBuffer positions = terrainBlock.getFloatBuffer(Type.Position);
+
+ // Prepare to cast rays
+ Vector3f pos = new Vector3f();
+ Vector3f dir = new Vector3f(0, -1, 0);
+ Ray ray = new Ray(pos, dir);
+
+ // Prepare collision results
+ CollisionResults results = new CollisionResults();
+
+ // Set the LOD indices on the block
+ VertexBuffer originalIndices = terrainBlock.getBuffer(Type.Index);
+
+ terrainBlock.clearBuffer(Type.Index);
+ terrainBlock.setBuffer(Type.Index, 3, lodIndices);
+
+ // Recalculate collision mesh
+ terrainBlock.createCollisionData();
+
+ float entropy = 0;
+ for (int i = 0; i < positions.capacity() / 3; i++){
+ BufferUtils.populateFromBuffer(pos, positions, i);
+
+ float realHeight = pos.y;
+
+ pos.addLocal(0, bbox.getYExtent(), 0);
+ ray.setOrigin(pos);
+
+ results.clear();
+ terrainBlock.collideWith(ray, Matrix4f.IDENTITY, bbox, results);
+
+ if (results.size() > 0){
+ Vector3f contactPoint = results.getClosestCollision().getContactPoint();
+ float delta = Math.abs(realHeight - contactPoint.y);
+ entropy = Math.max(delta, entropy);
+ }
+ }
+
+ // Restore original indices
+ terrainBlock.clearBuffer(Type.Index);
+ terrainBlock.setBuffer(originalIndices);
+
+ return entropy;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamTerrainPicker.java b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamTerrainPicker.java
new file mode 100644
index 0000000..9f044c6
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamTerrainPicker.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.picking;
+
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.Ray;
+import com.jme3.math.Triangle;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.geomipmap.picking.BresenhamYUpGridTracer.Direction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * It basically works by casting a pick ray
+ * against the bounding volumes of the TerrainQuad and its children, gathering
+ * all of the TerrainPatches hit (in distance order.) The triangles of each patch
+ * are then tested using the BresenhamYUpGridTracer to determine which triangles
+ * to test and in what order. When a hit is found, it is guaranteed to be the
+ * first such hit and can immediately be returned.
+ *
+ * @author Joshua Slack
+ * @author Brent Owens
+ */
+public class BresenhamTerrainPicker implements TerrainPicker {
+
+ private final Triangle gridTriA = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());
+ private final Triangle gridTriB = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());
+
+ private final Vector3f calcVec1 = new Vector3f();
+ private final Ray workRay = new Ray();
+ private final Ray worldPickRay = new Ray();
+
+ private final TerrainQuad root;
+ private final BresenhamYUpGridTracer tracer = new BresenhamYUpGridTracer();
+
+
+ public BresenhamTerrainPicker(TerrainQuad root) {
+ this.root = root;
+ }
+
+ public Vector3f getTerrainIntersection(Ray worldPick, CollisionResults results) {
+
+ worldPickRay.set(worldPick);
+ List<TerrainPickData> pickData = new ArrayList<TerrainPickData>();
+ root.findPick(worldPick.clone(), pickData);
+ Collections.sort(pickData);
+
+ if (pickData.isEmpty())
+ return null;
+
+ workRay.set(worldPick);
+
+ for (TerrainPickData pd : pickData) {
+ TerrainPatch patch = pd.targetPatch;
+
+
+ tracer.getGridSpacing().set(patch.getWorldScale());
+ tracer.setGridOrigin(patch.getWorldTranslation());
+
+ workRay.getOrigin().set(worldPick.getDirection()).multLocal(pd.cr.getDistance()-.1f).addLocal(worldPick.getOrigin());
+
+ tracer.startWalk(workRay);
+
+ final Vector3f intersection = new Vector3f();
+ final Vector2f loc = tracer.getGridLocation();
+
+ if (tracer.isRayPerpendicularToGrid()) {
+ Triangle hit = new Triangle();
+ checkTriangles(loc.x, loc.y, workRay, intersection, patch, hit);
+ float distance = worldPickRay.origin.distance(intersection);
+ CollisionResult cr = new CollisionResult(intersection, distance);
+ cr.setGeometry(patch);
+ cr.setContactNormal(hit.getNormal());
+ results.addCollision(cr);
+ return intersection;
+ }
+
+
+
+ while (loc.x >= -1 && loc.x <= patch.getSize() &&
+ loc.y >= -1 && loc.y <= patch.getSize()) {
+
+ //System.out.print(loc.x+","+loc.y+" : ");
+ // check the triangles of main square for intersection.
+ Triangle hit = new Triangle();
+ if (checkTriangles(loc.x, loc.y, workRay, intersection, patch, hit)) {
+ // we found an intersection, so return that!
+ float distance = worldPickRay.origin.distance(intersection);
+ CollisionResult cr = new CollisionResult(intersection, distance);
+ cr.setGeometry(patch);
+ results.addCollision(cr);
+ cr.setContactNormal(hit.getNormal());
+ return intersection;
+ }
+
+ // because of how we get our height coords, we will
+ // sometimes be off by a grid spot, so we check the next
+ // grid space up.
+ int dx = 0, dz = 0;
+ Direction d = tracer.getLastStepDirection();
+ switch (d) {
+ case PositiveX:
+ case NegativeX:
+ dx = 0;
+ dz = 1;
+ break;
+ case PositiveZ:
+ case NegativeZ:
+ dx = 1;
+ dz = 0;
+ break;
+ }
+
+ if (checkTriangles(loc.x + dx, loc.y + dz, workRay, intersection, patch, hit)) {
+ // we found an intersection, so return that!
+ float distance = worldPickRay.origin.distance(intersection);
+ CollisionResult cr = new CollisionResult(intersection, distance);
+ results.addCollision(cr);
+ cr.setGeometry(patch);
+ cr.setContactNormal(hit.getNormal());
+ return intersection;
+ }
+
+ tracer.next();
+ }
+ }
+
+ return null;
+ }
+
+ protected boolean checkTriangles(float gridX, float gridY, Ray pick, Vector3f intersection, TerrainPatch patch, Triangle store) {
+ if (!getTriangles(gridX, gridY, patch))
+ return false;
+
+ if (pick.intersectWhere(gridTriA, intersection)) {
+ store.set(gridTriA.get1(), gridTriA.get2(), gridTriA.get3());
+ return true;
+ } else {
+ if (pick.intersectWhere(gridTriB, intersection)) {
+ store.set(gridTriB.get1(), gridTriB.get2(), gridTriB.get3());
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Request the triangles (in world coord space) of a TerrainBlock that
+ * correspond to the given grid location. The triangles are stored in the
+ * class fields _gridTriA and _gridTriB.
+ *
+ * @param gridX
+ * grid row
+ * @param gridY
+ * grid column
+ * @param block
+ * the TerrainBlock we are working with
+ * @return true if the grid point is valid for the given block, false if it
+ * is off the block.
+ */
+ protected boolean getTriangles(float gridX, float gridY, TerrainPatch patch) {
+ calcVec1.set(gridX, 0, gridY);
+ int index = findClosestHeightIndex(calcVec1, patch);
+
+ if (index == -1)
+ return false;
+
+ Triangle[] t = patch.getGridTriangles(gridX, gridY);
+ if (t == null || t.length == 0)
+ return false;
+
+ gridTriA.set1(t[0].get1());
+ gridTriA.set2(t[0].get2());
+ gridTriA.set3(t[0].get3());
+
+ gridTriB.set1(t[1].get1());
+ gridTriB.set2(t[1].get2());
+ gridTriB.set3(t[1].get3());
+
+ return true;
+ }
+
+ /**
+ * Finds the closest height point to a position. Will always be left/above
+ * that position.
+ *
+ * @param position
+ * the position to check at
+ * @param block
+ * the block to get height values from
+ * @return an index to the height position of the given block.
+ */
+ protected int findClosestHeightIndex(Vector3f position, TerrainPatch patch) {
+
+ int x = (int) position.x;
+ int z = (int) position.z;
+
+ if (x < 0 || x >= patch.getSize() - 1) {
+ return -1;
+ }
+ if (z < 0 || z >= patch.getSize() - 1) {
+ return -1;
+ }
+
+ return z * patch.getSize() + x;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamYUpGridTracer.java b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamYUpGridTracer.java
new file mode 100644
index 0000000..8d8a53d
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/BresenhamYUpGridTracer.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.picking;
+
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+
+/**
+ * Works on the XZ plane, with positive Y as up.
+ *
+ * @author Joshua Slack
+ * @author Brent Owens
+ */
+public class BresenhamYUpGridTracer {
+
+ protected Vector3f gridOrigin = new Vector3f();
+ protected Vector3f gridSpacing = new Vector3f();
+ protected Vector2f gridLocation = new Vector2f();
+ protected Vector3f rayLocation = new Vector3f();
+ protected Ray walkRay = new Ray();
+
+ protected Direction stepDirection = Direction.None;
+ protected float rayLength;
+
+ public static enum Direction {
+ None, PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ;
+ };
+
+ // a "near zero" value we will use to determine if the walkRay is
+ // perpendicular to the grid.
+ protected static float TOLERANCE = 0.0000001f;
+
+ private int stepXDirection;
+ private int stepZDirection;
+
+ // from current position along ray
+ private float distToNextXIntersection, distToNextZIntersection;
+ private float distBetweenXIntersections, distBetweenZIntersections;
+
+ public void startWalk(final Ray walkRay) {
+ // store ray
+ this.walkRay.set(walkRay);
+
+ // simplify access to direction
+ Vector3f direction = this.walkRay.getDirection();
+
+ // Move start point to grid space
+ Vector3f start = this.walkRay.getOrigin().subtract(gridOrigin);
+
+ gridLocation.x = (int) (start.x / gridSpacing.x);
+ gridLocation.y = (int) (start.z / gridSpacing.z);
+
+ Vector3f ooDirection = new Vector3f(1.0f / direction.x, 1,1.0f / direction.z);
+
+ // Check which direction on the X world axis we are moving.
+ if (direction.x > TOLERANCE) {
+ distToNextXIntersection = ((gridLocation.x + 1) * gridSpacing.x - start.x) * ooDirection.x;
+ distBetweenXIntersections = gridSpacing.x * ooDirection.x;
+ stepXDirection = 1;
+ } else if (direction.x < -TOLERANCE) {
+ distToNextXIntersection = (start.x - (gridLocation.x * gridSpacing.x)) * -direction.x;
+ distBetweenXIntersections = -gridSpacing.x * ooDirection.x;
+ stepXDirection = -1;
+ } else {
+ distToNextXIntersection = Float.MAX_VALUE;
+ distBetweenXIntersections = Float.MAX_VALUE;
+ stepXDirection = 0;
+ }
+
+ // Check which direction on the Z world axis we are moving.
+ if (direction.z > TOLERANCE) {
+ distToNextZIntersection = ((gridLocation.y + 1) * gridSpacing.z - start.z) * ooDirection.z;
+ distBetweenZIntersections = gridSpacing.z * ooDirection.z;
+ stepZDirection = 1;
+ } else if (direction.z < -TOLERANCE) {
+ distToNextZIntersection = (start.z - (gridLocation.y * gridSpacing.z)) * -direction.z;
+ distBetweenZIntersections = -gridSpacing.z * ooDirection.z;
+ stepZDirection = -1;
+ } else {
+ distToNextZIntersection = Float.MAX_VALUE;
+ distBetweenZIntersections = Float.MAX_VALUE;
+ stepZDirection = 0;
+ }
+
+ // Reset some variables
+ rayLocation.set(start);
+ rayLength = 0.0f;
+ stepDirection = Direction.None;
+ }
+
+ public void next() {
+ // Walk us to our next location based on distances to next X or Z grid
+ // line.
+ if (distToNextXIntersection < distToNextZIntersection) {
+ rayLength = distToNextXIntersection;
+ gridLocation.x += stepXDirection;
+ distToNextXIntersection += distBetweenXIntersections;
+ switch (stepXDirection) {
+ case -1:
+ stepDirection = Direction.NegativeX;
+ break;
+ case 0:
+ stepDirection = Direction.None;
+ break;
+ case 1:
+ stepDirection = Direction.PositiveX;
+ break;
+ }
+ } else {
+ rayLength = distToNextZIntersection;
+ gridLocation.y += stepZDirection;
+ distToNextZIntersection += distBetweenZIntersections;
+ switch (stepZDirection) {
+ case -1:
+ stepDirection = Direction.NegativeZ;
+ break;
+ case 0:
+ stepDirection = Direction.None;
+ break;
+ case 1:
+ stepDirection = Direction.PositiveZ;
+ break;
+ }
+ }
+
+ rayLocation.set(walkRay.direction).multLocal(rayLength).addLocal(walkRay.origin);
+ }
+
+ public Direction getLastStepDirection() {
+ return stepDirection;
+ }
+
+ public boolean isRayPerpendicularToGrid() {
+ return stepXDirection == 0 && stepZDirection == 0;
+ }
+
+
+ public Vector2f getGridLocation() {
+ return gridLocation;
+ }
+
+ public Vector3f getGridOrigin() {
+ return gridOrigin;
+ }
+
+ public Vector3f getGridSpacing() {
+ return gridSpacing;
+ }
+
+
+ public void setGridLocation(Vector2f gridLocation) {
+ this.gridLocation = gridLocation;
+ }
+
+ public void setGridOrigin(Vector3f gridOrigin) {
+ this.gridOrigin = gridOrigin;
+ }
+
+ public void setGridSpacing(Vector3f gridSpacing) {
+ this.gridSpacing = gridSpacing;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPickData.java b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPickData.java
new file mode 100644
index 0000000..bee7623
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPickData.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.picking;
+
+import com.jme3.collision.CollisionResult;
+import com.jme3.terrain.geomipmap.TerrainPatch;
+
+/**
+ * Pick result on a terrain patch with the intersection on the bounding box
+ * of that terrain patch.
+ *
+ * @author Brent Owens
+ */
+public class TerrainPickData implements Comparable {
+
+ protected TerrainPatch targetPatch;
+ protected CollisionResult cr;
+
+ public TerrainPickData() {
+ }
+
+ public TerrainPickData(TerrainPatch patch, CollisionResult cr) {
+ this.targetPatch = patch;
+ this.cr = cr;
+ }
+
+ public int compareTo(Object o) {
+ if (o instanceof TerrainPickData) {
+ TerrainPickData tpd = (TerrainPickData) o;
+ if (this.cr.getDistance() < tpd.cr.getDistance())
+ return -1;
+ else if (this.cr.getDistance() == tpd.cr.getDistance())
+ return 0;
+ else
+ return 1;
+ }
+
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj instanceof TerrainPickData){
+ return ((TerrainPickData)obj).compareTo(this) == 0;
+ }
+ return super.equals(obj);
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPicker.java b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPicker.java
new file mode 100644
index 0000000..67df8e7
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/picking/TerrainPicker.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.geomipmap.picking;
+
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+
+/**
+ * Pick the location on the terrain from a given ray.
+ *
+ * @author Brent Owens
+ */
+public interface TerrainPicker {
+
+ /**
+ * Ask for the point of intersection between the given ray and the terrain.
+ *
+ * @param worldPick
+ * our pick ray, in world space.
+ * @return null if no pick is found. Otherwise it returns a Vector3f populated with the pick
+ * coordinates.
+ */
+ public Vector3f getTerrainIntersection(final Ray worldPick, CollisionResults results);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/AbstractHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/AbstractHeightMap.java
new file mode 100644
index 0000000..3193f52
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/AbstractHeightMap.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import java.io.DataOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <code>AbstractHeightMap</code> provides a base implementation of height
+ * data for terrain rendering. The loading of the data is dependent on the
+ * subclass. The abstract implementation provides a means to retrieve the height
+ * data and to save it.
+ *
+ * It is the general contract that any subclass provide a means of editing
+ * required attributes and calling <code>load</code> again to recreate a
+ * heightfield with these new parameters.
+ *
+ * @author Mark Powell
+ * @version $Id: AbstractHeightMap.java 4133 2009-03-19 20:40:11Z blaine.dev $
+ */
+public abstract class AbstractHeightMap implements HeightMap {
+
+ private static final Logger logger = Logger.getLogger(AbstractHeightMap.class.getName());
+ /** Height data information. */
+ protected float[] heightData = null;
+ /** The size of the height map's width. */
+ protected int size = 0;
+ /** Allows scaling the Y height of the map. */
+ protected float heightScale = 1.0f;
+ /** The filter is used to erode the terrain. */
+ protected float filter = 0.5f;
+ /** The range used to normalize terrain */
+ public static float NORMALIZE_RANGE = 255f;
+
+ /**
+ * <code>unloadHeightMap</code> clears the data of the height map. This
+ * insures it is ready for reloading.
+ */
+ public void unloadHeightMap() {
+ heightData = null;
+ }
+
+ /**
+ * <code>setHeightScale</code> sets the scale of the height values.
+ * Typically, the height is a little too extreme and should be scaled to a
+ * smaller value (i.e. 0.25), to produce cleaner slopes.
+ *
+ * @param scale
+ * the scale to multiply height values by.
+ */
+ public void setHeightScale(float scale) {
+ heightScale = scale;
+ }
+
+ /**
+ * <code>setHeightAtPoint</code> sets the height value for a given
+ * coordinate. It is recommended that the height value be within the 0 - 255
+ * range.
+ *
+ * @param height
+ * the new height for the coordinate.
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ */
+ public void setHeightAtPoint(float height, int x, int z) {
+ heightData[x + (z * size)] = height;
+ }
+
+ /**
+ * <code>setSize</code> sets the size of the terrain where the area is
+ * size x size.
+ *
+ * @param size
+ * the new size of the terrain.
+ * @throws Exception
+ *
+ * @throws JmeException
+ * if the size is less than or equal to zero.
+ */
+ public void setSize(int size) throws Exception {
+ if (size <= 0) {
+ throw new Exception("size must be greater than zero.");
+ }
+
+ this.size = size;
+ }
+
+ /**
+ * <code>setFilter</code> sets the erosion value for the filter. This
+ * value must be between 0 and 1, where 0.2 - 0.4 produces arguably the best
+ * results.
+ *
+ * @param filter
+ * the erosion value.
+ * @throws Exception
+ * @throws JmeException
+ * if filter is less than 0 or greater than 1.
+ */
+ public void setMagnificationFilter(float filter) throws Exception {
+ if (filter < 0 || filter >= 1) {
+ throw new Exception("filter must be between 0 and 1");
+ }
+ this.filter = filter;
+ }
+
+ /**
+ * <code>getTrueHeightAtPoint</code> returns the non-scaled value at the
+ * point provided.
+ *
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ * @return the value at (x,z).
+ */
+ public float getTrueHeightAtPoint(int x, int z) {
+ //logger.info( heightData[x + (z*size)]);
+ return heightData[x + (z * size)];
+ }
+
+ /**
+ * <code>getScaledHeightAtPoint</code> returns the scaled value at the
+ * point provided.
+ *
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ * @return the scaled value at (x, z).
+ */
+ public float getScaledHeightAtPoint(int x, int z) {
+ return ((heightData[x + (z * size)]) * heightScale);
+ }
+
+ /**
+ * <code>getInterpolatedHeight</code> returns the height of a point that
+ * does not fall directly on the height posts.
+ *
+ * @param x
+ * the x coordinate of the point.
+ * @param z
+ * the y coordinate of the point.
+ * @return the interpolated height at this point.
+ */
+ public float getInterpolatedHeight(float x, float z) {
+ float low, highX, highZ;
+ float intX, intZ;
+ float interpolation;
+
+ low = getScaledHeightAtPoint((int) x, (int) z);
+
+ if (x + 1 >= size) {
+ return low;
+ }
+
+ highX = getScaledHeightAtPoint((int) x + 1, (int) z);
+
+ interpolation = x - (int) x;
+ intX = ((highX - low) * interpolation) + low;
+
+ if (z + 1 >= size) {
+ return low;
+ }
+
+ highZ = getScaledHeightAtPoint((int) x, (int) z + 1);
+
+ interpolation = z - (int) z;
+ intZ = ((highZ - low) * interpolation) + low;
+
+ return ((intX + intZ) / 2);
+ }
+
+ /**
+ * <code>getHeightMap</code> returns the entire grid of height data.
+ *
+ * @return the grid of height data.
+ */
+ public float[] getHeightMap() {
+ return heightData;
+ }
+
+ /**
+ * Build a new array of height data with the scaled values.
+ * @return
+ */
+ public float[] getScaledHeightMap() {
+ float[] hm = new float[heightData.length];
+ for (int i=0; i<heightData.length; i++) {
+ hm[i] = heightScale * heightData[i];
+ }
+ return hm;
+ }
+
+ /**
+ * <code>getSize</code> returns the size of one side the height map. Where
+ * the area of the height map is size x size.
+ *
+ * @return the size of a single side.
+ */
+ public int getSize() {
+ return size;
+ }
+
+ /**
+ * <code>save</code> will save the heightmap data into a new RAW file
+ * denoted by the supplied filename.
+ *
+ * @param filename
+ * the file name to save the current data as.
+ * @return true if the save was successful, false otherwise.
+ * @throws Exception
+ *
+ * @throws JmeException
+ * if filename is null.
+ */
+ public boolean save(String filename) throws Exception {
+ if (null == filename) {
+ throw new Exception("Filename must not be null");
+ }
+ //open the streams and send the height data to the file.
+ try {
+ FileOutputStream fos = new FileOutputStream(filename);
+ DataOutputStream dos = new DataOutputStream(fos);
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ dos.write((int) heightData[j + (i * size)]);
+ }
+ }
+
+ fos.close();
+ dos.close();
+ } catch (FileNotFoundException e) {
+ logger.log(Level.WARNING, "Error opening file {0}", filename);
+ return false;
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Error writing to file {0}", filename);
+ return false;
+ }
+
+ logger.log(Level.INFO, "Saved terrain to {0}", filename);
+ return true;
+ }
+
+ /**
+ * <code>normalizeTerrain</code> takes the current terrain data and
+ * converts it to values between 0 and <code>value</code>.
+ *
+ * @param value
+ * the value to normalize to.
+ */
+ public void normalizeTerrain(float value) {
+ float currentMin, currentMax;
+ float height;
+
+ currentMin = heightData[0];
+ currentMax = heightData[0];
+
+ //find the min/max values of the height fTemptemptempBuffer
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ if (heightData[i + j * size] > currentMax) {
+ currentMax = heightData[i + j * size];
+ } else if (heightData[i + j * size] < currentMin) {
+ currentMin = heightData[i + j * size];
+ }
+ }
+ }
+
+ //find the range of the altitude
+ if (currentMax <= currentMin) {
+ return;
+ }
+
+ height = currentMax - currentMin;
+
+ //scale the values to a range of 0-255
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ heightData[i + j * size] = ((heightData[i + j * size] - currentMin) / height) * value;
+ }
+ }
+ }
+
+ /**
+ * Find the minimum and maximum height values.
+ * @return a float array with two value: min height, max height
+ */
+ public float[] findMinMaxHeights() {
+ float[] minmax = new float[2];
+
+ float currentMin, currentMax;
+ currentMin = heightData[0];
+ currentMax = heightData[0];
+
+ for (int i = 0; i < heightData.length; i++) {
+ if (heightData[i] > currentMax) {
+ currentMax = heightData[i];
+ } else if (heightData[i] < currentMin) {
+ currentMin = heightData[i];
+ }
+ }
+ minmax[0] = currentMin;
+ minmax[1] = currentMax;
+ return minmax;
+ }
+
+ /**
+ * <code>erodeTerrain</code> is a convenience method that applies the FIR
+ * filter to a given height map. This simulates water errosion.
+ *
+ * @see setFilter
+ */
+ public void erodeTerrain() {
+ //erode left to right
+ float v;
+
+ for (int i = 0; i < size; i++) {
+ v = heightData[i];
+ for (int j = 1; j < size; j++) {
+ heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
+ v = heightData[i + j * size];
+ }
+ }
+
+ //erode right to left
+ for (int i = size - 1; i >= 0; i--) {
+ v = heightData[i];
+ for (int j = 0; j < size; j++) {
+ heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
+ v = heightData[i + j * size];
+ //erodeBand(tempBuffer[size * i + size - 1], -1);
+ }
+ }
+
+ //erode top to bottom
+ for (int i = 0; i < size; i++) {
+ v = heightData[i];
+ for (int j = 0; j < size; j++) {
+ heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
+ v = heightData[i + j * size];
+ }
+ }
+
+ //erode from bottom to top
+ for (int i = size - 1; i >= 0; i--) {
+ v = heightData[i];
+ for (int j = 0; j < size; j++) {
+ heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
+ v = heightData[i + j * size];
+ }
+ }
+ }
+
+ /**
+ * Flattens out the valleys. The flatten algorithm makes the valleys more
+ * prominent while keeping the hills mostly intact. This effect is based on
+ * what happens when values below one are squared. The terrain will be
+ * normalized between 0 and 1 for this function to work.
+ *
+ * @param flattening
+ * the power of flattening applied, 1 means none
+ */
+ public void flatten(byte flattening) {
+ // If flattening is one we can skip the calculations
+ // since it wouldn't change anything. (e.g. 2 power 1 = 2)
+ if (flattening <= 1) {
+ return;
+ }
+
+ float[] minmax = findMinMaxHeights();
+
+ normalizeTerrain(1f);
+
+ for (int x = 0; x < size; x++) {
+ for (int y = 0; y < size; y++) {
+ float flat = 1.0f;
+ float original = heightData[x + y * size];
+
+ // Flatten as many times as desired;
+ for (int i = 0; i < flattening; i++) {
+ flat *= original;
+ }
+ heightData[x + y * size] = flat;
+ }
+ }
+
+ // re-normalize back to its oraginal height range
+ float height = minmax[1] - minmax[0];
+ normalizeTerrain(height);
+ }
+
+ /**
+ * Smooth the terrain. For each node, its 8 neighbors heights
+ * are averaged and will participate in the node new height
+ * by a factor <code>np</code> between 0 and 1
+ *
+ * You must first load() the heightmap data before this will have any effect.
+ *
+ * @param np
+ * The factor to what extend the neighbors average has an influence.
+ * Value of 0 will ignore neighbors (no smoothing)
+ * Value of 1 will ignore the node old height.
+ */
+ public void smooth(float np) {
+ smooth(np, 1);
+ }
+
+ /**
+ * Smooth the terrain. For each node, its X(determined by radius) neighbors heights
+ * are averaged and will participate in the node new height
+ * by a factor <code>np</code> between 0 and 1
+ *
+ * You must first load() the heightmap data before this will have any effect.
+ *
+ * @param np
+ * The factor to what extend the neighbors average has an influence.
+ * Value of 0 will ignore neighbors (no smoothing)
+ * Value of 1 will ignore the node old height.
+ */
+ public void smooth(float np, int radius) {
+ if (np < 0 || np > 1) {
+ return;
+ }
+ if (radius == 0)
+ radius = 1;
+
+ for (int x = 0; x < size; x++) {
+ for (int y = 0; y < size; y++) {
+ int neighNumber = 0;
+ float neighAverage = 0;
+ for (int rx = -radius; rx <= radius; rx++) {
+ for (int ry = -radius; ry <= radius; ry++) {
+ if (x+rx < 0 || x+rx >= size) {
+ continue;
+ }
+ if (y+ry < 0 || y+ry >= size) {
+ continue;
+ }
+ neighNumber++;
+ neighAverage += heightData[(x+rx) + (y+ry) * size];
+ }
+ }
+
+ neighAverage /= neighNumber;
+ float cp = 1 - np;
+ heightData[x + y * size] = neighAverage * np + heightData[x + y * size] * cp;
+ }
+ }
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/CombinerHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/CombinerHeightMap.java
new file mode 100644
index 0000000..7a25834
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/CombinerHeightMap.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import java.util.logging.Logger;
+
+/**
+ * <code>CombinerHeightMap</code> generates a new height map based on
+ * two provided height maps. These had maps can either be added together
+ * or substracted from each other. Each heightmap has a weight to
+ * determine how much one will affect the other. By default it is set to
+ * 0.5, 0.5 and meaning the two heightmaps are averaged evenly. This
+ * value can be adjusted at will, as long as the two factors are equal
+ * to 1.0.
+ *
+ * @author Mark Powell
+ * @version $Id$
+ */
+public class CombinerHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(CombinerHeightMap.class.getName());
+ /**
+ * Constant mode to denote adding the two heightmaps.
+ */
+ public static final int ADDITION = 0;
+ /**
+ * Constant mode to denote subtracting the two heightmaps.
+ */
+ public static final int SUBTRACTION = 1;
+ //the two maps.
+ private AbstractHeightMap map1;
+ private AbstractHeightMap map2;
+ //the two factors
+ private float factor1 = 0.5f;
+ private float factor2 = 0.5f;
+ //the combine mode.
+ private int mode;
+
+ /**
+ * Constructor combines two given heightmaps by the specified mode.
+ * The heightmaps will be evenly distributed. The heightmaps
+ * must be of the same size.
+ *
+ * @param map1 the first heightmap to combine.
+ * @param map2 the second heightmap to combine.
+ * @param mode denotes whether to add or subtract the heightmaps, may
+ * be either ADDITION or SUBTRACTION.
+ * @throws JmeException if either map is null, their size
+ * do not match or the mode is invalid.
+ */
+ public CombinerHeightMap(
+ AbstractHeightMap map1,
+ AbstractHeightMap map2,
+ int mode) throws Exception {
+
+
+ //insure all parameters are valid.
+ if (null == map1 || null == map2) {
+ throw new Exception("Height map may not be null");
+ }
+
+
+ if (map1.getSize() != map2.getSize()) {
+ throw new Exception("The two maps must be of the same size");
+ }
+
+
+ if ((factor1 + factor2) != 1.0f) {
+ throw new Exception("factor1 and factor2 must add to 1.0");
+ }
+
+
+ this.size = map1.getSize();
+ this.map1 = map1;
+ this.map2 = map2;
+
+
+ setMode(mode);
+
+ load();
+ }
+
+ /**
+ * Constructor combines two given heightmaps by the specified mode.
+ * The heightmaps will be distributed based on the given factors.
+ * For example, if factor1 is 0.6 and factor2 is 0.4, then 60% of
+ * map1 will be used with 40% of map2. The two factors must add up
+ * to 1.0. The heightmaps must also be of the same size.
+ *
+ * @param map1 the first heightmap to combine.
+ * @param factor1 the factor for map1.
+ * @param map2 the second heightmap to combine.
+ * @param factor2 the factor for map2.
+ * @param mode denotes whether to add or subtract the heightmaps, may
+ * be either ADDITION or SUBTRACTION.
+ * @throws JmeException if either map is null, their size
+ * do not match, the mode is invalid, or the factors do not add
+ * to 1.0.
+ */
+ public CombinerHeightMap(
+ AbstractHeightMap map1,
+ float factor1,
+ AbstractHeightMap map2,
+ float factor2,
+ int mode) throws Exception {
+
+
+ //insure all parameters are valid.
+ if (null == map1 || null == map2) {
+ throw new Exception("Height map may not be null");
+ }
+
+
+ if (map1.getSize() != map2.getSize()) {
+ throw new Exception("The two maps must be of the same size");
+ }
+
+
+ if ((factor1 + factor2) != 1.0f) {
+ throw new Exception("factor1 and factor2 must add to 1.0");
+ }
+
+
+ setMode(mode);
+
+
+ this.size = map1.getSize();
+ this.map1 = map1;
+ this.map2 = map2;
+ this.factor1 = factor1;
+ this.factor2 = factor2;
+
+
+ this.mode = mode;
+
+
+ load();
+ }
+
+ /**
+ * <code>setFactors</code> sets the distribution of heightmaps.
+ * For example, if factor1 is 0.6 and factor2 is 0.4, then 60% of
+ * map1 will be used with 40% of map2. The two factors must add up
+ * to 1.0.
+ * @param factor1 the factor for map1.
+ * @param factor2 the factor for map2.
+ * @throws JmeException if the factors do not add to 1.0.
+ */
+ public void setFactors(float factor1, float factor2) throws Exception {
+ if ((factor1 + factor2) != 1.0f) {
+ throw new Exception("factor1 and factor2 must add to 1.0");
+ }
+
+
+ this.factor1 = factor1;
+ this.factor2 = factor2;
+ }
+
+ /**
+ * <code>setHeightMaps</code> sets the height maps to combine.
+ * The size of the height maps must be the same.
+ * @param map1 the first height map.
+ * @param map2 the second height map.
+ * @throws JmeException if the either heightmap is null, or their
+ * sizes do not match.
+ */
+ public void setHeightMaps(AbstractHeightMap map1, AbstractHeightMap map2) throws Exception {
+ if (null == map1 || null == map2) {
+ throw new Exception("Height map may not be null");
+ }
+
+
+ if (map1.getSize() != map2.getSize()) {
+ throw new Exception("The two maps must be of the same size");
+ }
+
+
+ this.size = map1.getSize();
+ this.map1 = map1;
+ this.map2 = map2;
+ }
+
+ /**
+ * <code>setMode</code> sets the mode of the combiner. This may either
+ * be ADDITION or SUBTRACTION.
+ * @param mode the mode of the combiner.
+ * @throws JmeException if mode is not ADDITION or SUBTRACTION.
+ */
+ public void setMode(int mode) throws Exception {
+ if (mode != ADDITION && mode != SUBTRACTION) {
+ throw new Exception("Invalid mode");
+ }
+ this.mode = mode;
+ }
+
+ /**
+ * <code>load</code> builds a new heightmap based on the combination of
+ * two other heightmaps. The conditions of the combiner determine the
+ * final outcome of the heightmap.
+ *
+ * @return boolean if the heightmap was successfully created.
+ */
+ public boolean load() {
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+
+
+ heightData = new float[size * size];
+
+
+ float[] temp1 = map1.getHeightMap();
+ float[] temp2 = map2.getHeightMap();
+
+
+ if (mode == ADDITION) {
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ heightData[i + (j * size)] =
+ (int) (temp1[i + (j * size)] * factor1
+ + temp2[i + (j * size)] * factor2);
+ }
+ }
+ } else if (mode == SUBTRACTION) {
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ heightData[i + (j * size)] =
+ (int) (temp1[i + (j * size)] * factor1
+ - temp2[i + (j * size)] * factor2);
+ }
+ }
+ }
+
+
+ logger.info("Created heightmap using Combiner");
+
+
+ return true;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/FaultHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/FaultHeightMap.java
new file mode 100644
index 0000000..194486a
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/FaultHeightMap.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.math.FastMath;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Creates an heightmap based on the fault algorithm. Each iteration, a random line
+ * crossing the map is generated. On one side height values are raised, on the other side
+ * lowered.
+ * @author cghislai
+ */
+public class FaultHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(FaultHeightMap.class.getName());
+ /**
+ * Values on one side are lowered, on the other side increased,
+ * creating a step at the fault line
+ */
+ public static final int FAULTTYPE_STEP = 0;
+ /**
+ * Values on one side are lowered, then increase lineary while crossing
+ * the fault line to the other side. The fault line will be a inclined
+ * plane
+ */
+ public static final int FAULTTYPE_LINEAR = 1;
+ /**
+ * Values are lowered on one side, increased on the other, creating a
+ * cosine curve on the fault line
+ */
+ public static final int FAULTTYPE_COSINE = 2;
+ /**
+ * Value are lowered on both side, but increased on the fault line
+ * creating a smooth ridge on the fault line.
+ */
+ public static final int FAULTTYPE_SINE = 3;
+ /**
+ * A linear fault is created
+ */
+ public static final int FAULTSHAPE_LINE = 10;
+ /**
+ * A circular fault is created.
+ */
+ public static final int FAULTSHAPE_CIRCLE = 11;
+ private long seed; // A seed to feed the random
+ private int iterations; // iterations to perform
+ private float minFaultHeight; // the height modification applied
+ private float maxFaultHeight; // the height modification applied
+ private float minRange; // The range for linear and trigo faults
+ private float maxRange; // The range for linear and trigo faults
+ private float minRadius; // radii for circular fault
+ private float maxRadius;
+ private int faultType; // The type of fault
+ private int faultShape; // The type of fault
+
+ /**
+ * Constructor creates the fault. For faulttype other than STEP, a range can
+ * be provided. For faultshape circle, min and max radii can be provided.
+ * Don't forget to reload the map if you have set parameters after the constructor
+ * call.
+ * @param size The size of the heightmap
+ * @param iterations Iterations to perform
+ * @param faultType Type of fault
+ * @param faultShape Shape of the fault -line or circle
+ * @param minFaultHeight Height modified on each side
+ * @param maxFaultHeight Height modified on each side
+ * @param seed A seed to feed the Random generator
+ * @see setFaultRange, setMinRadius, setMaxRadius
+ */
+ public FaultHeightMap(int size, int iterations, int faultType, int faultShape, float minFaultHeight, float maxFaultHeight, long seed) throws Exception {
+ if (size < 0 || iterations < 0) {
+ throw new Exception("Size and iterations must be greater than 0!");
+ }
+ this.size = size;
+ this.iterations = iterations;
+ this.faultType = faultType;
+ this.faultShape = faultShape;
+ this.minFaultHeight = minFaultHeight;
+ this.maxFaultHeight = maxFaultHeight;
+ this.seed = seed;
+ this.minRange = minFaultHeight;
+ this.maxRange = maxFaultHeight;
+ this.minRadius = size / 10;
+ this.maxRadius = size / 4;
+ load();
+ }
+
+ /**
+ * Create an heightmap with linear step faults.
+ * @param size size of heightmap
+ * @param iterations number of iterations
+ * @param minFaultHeight Height modified on each side
+ * @param maxFaultHeight Height modified on each side
+ */
+ public FaultHeightMap(int size, int iterations, float minFaultHeight, float maxFaultHeight) throws Exception {
+ this(size, iterations, FAULTTYPE_STEP, FAULTSHAPE_LINE, minFaultHeight, maxFaultHeight, new Random().nextLong());
+ }
+
+ @Override
+ public boolean load() {
+ // clean up data if needed.
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+ heightData = new float[size * size];
+ float[][] tempBuffer = new float[size][size];
+ Random random = new Random(seed);
+
+ for (int i = 0; i < iterations; i++) {
+ addFault(tempBuffer, random);
+ }
+
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ setHeightAtPoint(tempBuffer[i][j], i, j);
+ }
+ }
+
+ normalizeTerrain(NORMALIZE_RANGE);
+
+ logger.log(Level.INFO, "Fault heightmap generated");
+ return true;
+ }
+
+ protected void addFault(float[][] tempBuffer, Random random) {
+ float faultHeight = minFaultHeight + random.nextFloat() * (maxFaultHeight - minFaultHeight);
+ float range = minRange + random.nextFloat() * (maxRange - minRange);
+ switch (faultShape) {
+ case FAULTSHAPE_LINE:
+ addLineFault(tempBuffer, random, faultHeight, range);
+ break;
+ case FAULTSHAPE_CIRCLE:
+ addCircleFault(tempBuffer, random, faultHeight, range);
+ break;
+ }
+ }
+
+ protected void addLineFault(float[][] tempBuffer, Random random, float faultHeight, float range) {
+ int x1 = random.nextInt(size);
+ int x2 = random.nextInt(size);
+ int y1 = random.nextInt(size);
+ int y2 = random.nextInt(size);
+
+
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ float dist = ((x2 - x1) * (j - y1) - (y2 - y1) * (i - x1))
+ / (FastMath.sqrt(FastMath.sqr(x2 - x1) + FastMath.sqr(y2 - y1)));
+ tempBuffer[i][j] += calcHeight(dist, random, faultHeight, range);
+ }
+ }
+ }
+
+ protected void addCircleFault(float[][] tempBuffer, Random random, float faultHeight, float range) {
+ float radius = random.nextFloat() * (maxRadius - minRadius) + minRadius;
+ int intRadius = (int) FastMath.floor(radius);
+ // Allox circle center to be out of map if not by more than radius.
+ // Unlucky cases will put them in the far corner, with the circle
+ // entirely outside heightmap
+ int x = random.nextInt(size + 2 * intRadius) - intRadius;
+ int y = random.nextInt(size + 2 * intRadius) - intRadius;
+
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ float dist;
+ if (i != x || j != y) {
+ int dx = i - x;
+ int dy = j - y;
+ float dmag = FastMath.sqrt(FastMath.sqr(dx) + FastMath.sqr(dy));
+ float rx = x + dx / dmag * radius;
+ float ry = y + dy / dmag * radius;
+ dist = FastMath.sign(dmag - radius)
+ * FastMath.sqrt(FastMath.sqr(i - rx) + FastMath.sqr(j - ry));
+ } else {
+ dist = 0;
+ }
+ tempBuffer[i][j] += calcHeight(dist, random, faultHeight, range);
+ }
+ }
+ }
+
+ protected float calcHeight(float dist, Random random, float faultHeight, float range) {
+ switch (faultType) {
+ case FAULTTYPE_STEP: {
+ return FastMath.sign(dist) * faultHeight;
+ }
+ case FAULTTYPE_LINEAR: {
+ if (FastMath.abs(dist) > range) {
+ return FastMath.sign(dist) * faultHeight;
+ }
+ float f = FastMath.abs(dist) / range;
+ return FastMath.sign(dist) * faultHeight * f;
+ }
+ case FAULTTYPE_SINE: {
+ if (FastMath.abs(dist) > range) {
+ return -faultHeight;
+ }
+ float f = dist / range;
+ // We want -1 at f=-1 and f=1; 1 at f=0
+ return FastMath.sin((1 + 2 * f) * FastMath.PI / 2) * faultHeight;
+ }
+ case FAULTTYPE_COSINE: {
+ if (FastMath.abs(dist) > range) {
+ return -FastMath.sign(dist) * faultHeight;
+ }
+ float f = dist / range;
+ float val = FastMath.cos((1 + f) * FastMath.PI / 2) * faultHeight;
+ return val;
+ }
+ }
+ //shoudn't go here
+ throw new RuntimeException("Code needs update to switch allcases");
+ }
+
+ public int getFaultShape() {
+ return faultShape;
+ }
+
+ public void setFaultShape(int faultShape) {
+ this.faultShape = faultShape;
+ }
+
+ public int getFaultType() {
+ return faultType;
+ }
+
+ public void setFaultType(int faultType) {
+ this.faultType = faultType;
+ }
+
+ public int getIterations() {
+ return iterations;
+ }
+
+ public void setIterations(int iterations) {
+ this.iterations = iterations;
+ }
+
+ public float getMaxFaultHeight() {
+ return maxFaultHeight;
+ }
+
+ public void setMaxFaultHeight(float maxFaultHeight) {
+ this.maxFaultHeight = maxFaultHeight;
+ }
+
+ public float getMaxRadius() {
+ return maxRadius;
+ }
+
+ public void setMaxRadius(float maxRadius) {
+ this.maxRadius = maxRadius;
+ }
+
+ public float getMaxRange() {
+ return maxRange;
+ }
+
+ public void setMaxRange(float maxRange) {
+ this.maxRange = maxRange;
+ }
+
+ public float getMinFaultHeight() {
+ return minFaultHeight;
+ }
+
+ public void setMinFaultHeight(float minFaultHeight) {
+ this.minFaultHeight = minFaultHeight;
+ }
+
+ public float getMinRadius() {
+ return minRadius;
+ }
+
+ public void setMinRadius(float minRadius) {
+ this.minRadius = minRadius;
+ }
+
+ public float getMinRange() {
+ return minRange;
+ }
+
+ public void setMinRange(float minRange) {
+ this.minRange = minRange;
+ }
+
+ public long getSeed() {
+ return seed;
+ }
+
+ public void setSeed(long seed) {
+ this.seed = seed;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/FluidSimHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/FluidSimHeightMap.java
new file mode 100644
index 0000000..ec2dfdf
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/FluidSimHeightMap.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import java.util.Random;
+import java.util.logging.Logger;
+
+/**
+ * <code>FluidSimHeightMap</code> generates a height map based using some
+ * sort of fluid simulation. The heightmap is treated as a highly viscous and
+ * rubbery fluid enabling to fine tune the generated heightmap using a number
+ * of parameters.
+ *
+ * @author Frederik Boelthoff
+ * @see <a href="http://www.gamedev.net/reference/articles/article2001.asp">Terrain Generation Using Fluid Simulation</a>
+ * @version $Id$
+ *
+ */
+public class FluidSimHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(FluidSimHeightMap.class.getName());
+ private float waveSpeed = 100.0f; // speed at which the waves travel
+ private float timeStep = 0.033f; // constant time-step between each iteration
+ private float nodeDistance = 10.0f; // distance between each node of the surface
+ private float viscosity = 100.0f; // viscosity of the fluid
+ private int iterations; // number of iterations
+ private float minInitialHeight = -500; // min initial height
+ private float maxInitialHeight = 500; // max initial height
+ private long seed; // the seed for the random number generator
+ float coefA, coefB, coefC; // pre-computed coefficients in the fluid equation
+
+ /**
+ * Constructor sets the attributes of the hill system and generates the
+ * height map. It gets passed a number of tweakable parameters which
+ * fine-tune the outcome.
+ *
+ * @param size
+ * size the size of the terrain to be generated
+ * @param iterations
+ * the number of iterations to do
+ * @param minInitialHeight
+ * the minimum initial height of a terrain value
+ * @param maxInitialHeight
+ * the maximum initial height of a terrain value
+ * @param viscosity
+ * the viscosity of the fluid
+ * @param waveSpeed
+ * the speed at which the waves travel
+ * @param timestep
+ * the constant time-step between each iteration
+ * @param nodeDistance
+ * the distance between each node of the heightmap
+ * @param seed
+ * the seed to generate the same heightmap again
+ * @throws JmeException
+ * if size of the terrain is not greater that zero, or number of
+ * iterations is not greater that zero, or the minimum initial height
+ * is greater than the maximum (or the other way around)
+ */
+ public FluidSimHeightMap(int size, int iterations, float minInitialHeight, float maxInitialHeight, float viscosity, float waveSpeed, float timestep, float nodeDistance, long seed) throws Exception {
+ if (size <= 0 || iterations <= 0 || minInitialHeight >= maxInitialHeight) {
+ throw new Exception(
+ "Either size of the terrain is not greater that zero, "
+ + "or number of iterations is not greater that zero, "
+ + "or minimum height greater or equal as the maximum, "
+ + "or maximum height smaller or equal as the minimum.");
+ }
+
+ this.size = size;
+ this.seed = seed;
+ this.iterations = iterations;
+ this.minInitialHeight = minInitialHeight;
+ this.maxInitialHeight = maxInitialHeight;
+ this.viscosity = viscosity;
+ this.waveSpeed = waveSpeed;
+ this.timeStep = timestep;
+ this.nodeDistance = nodeDistance;
+
+ load();
+ }
+
+ /**
+ * Constructor sets the attributes of the hill system and generates the
+ * height map.
+ *
+ * @param size
+ * size the size of the terrain to be generated
+ * @param iterations
+ * the number of iterations to do
+ * @throws JmeException
+ * if size of the terrain is not greater that zero, or number of
+ * iterations is not greater that zero
+ */
+ public FluidSimHeightMap(int size, int iterations) throws Exception {
+ if (size <= 0 || iterations <= 0) {
+ throw new Exception(
+ "Either size of the terrain is not greater that zero, "
+ + "or number of iterations is not greater that zero");
+ }
+
+ this.size = size;
+ this.iterations = iterations;
+
+ load();
+ }
+
+
+ /*
+ * Generates a heightmap using fluid simulation and the attributes set by
+ * the constructor or the setters.
+ */
+ public boolean load() {
+ // Clean up data if needed.
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+
+ heightData = new float[size * size];
+ float[][] tempBuffer = new float[2][size * size];
+ Random random = new Random(seed);
+
+ // pre-compute the coefficients in the fluid equation
+ coefA = (4 - (8 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
+ coefB = (viscosity * timeStep - 2) / (viscosity * timeStep + 2);
+ coefC = ((2 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
+
+ // initialize the heightmaps to random values except for the edges
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ tempBuffer[0][j + i * size] = tempBuffer[1][j + i * size] = randomRange(random, minInitialHeight, maxInitialHeight);
+ }
+ }
+
+ int curBuf = 0;
+ int ind;
+
+ float[] oldBuffer;
+ float[] newBuffer;
+
+ // Iterate over the heightmap, applying the fluid simulation equation.
+ // Although it requires knowledge of the two previous timesteps, it only
+ // accesses one pixel of the k-1 timestep, so using a simple trick we only
+ // need to store the heightmap twice, not three times, and we can avoid
+ // copying data every iteration.
+ for (int i = 0; i < iterations; i++) {
+ oldBuffer = tempBuffer[1 - curBuf];
+ newBuffer = tempBuffer[curBuf];
+
+ for (int y = 0; y < size; y++) {
+ for (int x = 0; x < size; x++) {
+ ind = x + y * size;
+ float neighborsValue = 0;
+ int neighbors = 0;
+
+ if (x > 0) {
+ neighborsValue += newBuffer[ind - 1];
+ neighbors++;
+ }
+ if (x < size - 1) {
+ neighborsValue += newBuffer[ind + 1];
+ neighbors++;
+ }
+ if (y > 0) {
+ neighborsValue += newBuffer[ind - size];
+ neighbors++;
+ }
+ if (y < size - 1) {
+ neighborsValue += newBuffer[ind + size];
+ neighbors++;
+ }
+ if (neighbors != 4) {
+ neighborsValue *= 4 / neighbors;
+ }
+ oldBuffer[ind] = coefA * newBuffer[ind] + coefB
+ * oldBuffer[ind] + coefC * (neighborsValue);
+ }
+ }
+
+ curBuf = 1 - curBuf;
+ }
+
+ // put the normalized heightmap into the range [0...255] and into the heightmap
+ for (int y = 0; y < size; y++) {
+ for (int x = 0; x < size; x++) {
+ heightData[x + y * size] = (float) (tempBuffer[curBuf][x + y * size]);
+ }
+ }
+ normalizeTerrain(NORMALIZE_RANGE);
+
+ logger.info("Created Heightmap using fluid simulation");
+
+ return true;
+ }
+
+ private float randomRange(Random random, float min, float max) {
+ return (random.nextFloat() * (max - min)) + min;
+ }
+
+ /**
+ * Sets the number of times the fluid simulation should be iterated over
+ * the heightmap. The more often this is, the less features (hills, etc)
+ * the terrain will have, and the smoother it will be.
+ *
+ * @param iterations
+ * the number of iterations to do
+ * @throws JmeException
+ * if iterations if not greater than zero
+ */
+ public void setIterations(int iterations) throws Exception {
+ if (iterations <= 0) {
+ throw new Exception(
+ "Number of iterations is not greater than zero");
+ }
+ this.iterations = iterations;
+ }
+
+ /**
+ * Sets the maximum initial height of the terrain.
+ *
+ * @param maxInitialHeight
+ * the maximum initial height
+ * @see #setMinInitialHeight(int)
+ */
+ public void setMaxInitialHeight(float maxInitialHeight) {
+ this.maxInitialHeight = maxInitialHeight;
+ }
+
+ /**
+ * Sets the minimum initial height of the terrain.
+ *
+ * @param minInitialHeight
+ * the minimum initial height
+ * @see #setMaxInitialHeight(int)
+ */
+ public void setMinInitialHeight(float minInitialHeight) {
+ this.minInitialHeight = minInitialHeight;
+ }
+
+ /**
+ * Sets the distance between each node of the heightmap.
+ *
+ * @param nodeDistance
+ * the distance between each node
+ */
+ public void setNodeDistance(float nodeDistance) {
+ this.nodeDistance = nodeDistance;
+ }
+
+ /**
+ * Sets the time-speed between each iteration of the fluid
+ * simulation algortithm.
+ *
+ * @param timeStep
+ * the time-step between each iteration
+ */
+ public void setTimeStep(float timeStep) {
+ this.timeStep = timeStep;
+ }
+
+ /**
+ * Sets the viscosity of the simulated fuid.
+ *
+ * @param viscosity
+ * the viscosity of the fluid
+ */
+ public void setViscosity(float viscosity) {
+ this.viscosity = viscosity;
+ }
+
+ /**
+ * Sets the speed at which the waves trave.
+ *
+ * @param waveSpeed
+ * the speed at which the waves travel
+ */
+ public void setWaveSpeed(float waveSpeed) {
+ this.waveSpeed = waveSpeed;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/HeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/HeightMap.java
new file mode 100644
index 0000000..1bbed36
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/HeightMap.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.heightmap;
+
+/**
+ *
+ * @author cghislai
+ */
+public interface HeightMap {
+
+ /**
+ * <code>getHeightMap</code> returns the entire grid of height data.
+ *
+ * @return the grid of height data.
+ */
+ float[] getHeightMap();
+
+ float[] getScaledHeightMap();
+
+ /**
+ * <code>getInterpolatedHeight</code> returns the height of a point that
+ * does not fall directly on the height posts.
+ *
+ * @param x
+ * the x coordinate of the point.
+ * @param z
+ * the y coordinate of the point.
+ * @return the interpolated height at this point.
+ */
+ float getInterpolatedHeight(float x, float z);
+
+ /**
+ * <code>getScaledHeightAtPoint</code> returns the scaled value at the
+ * point provided.
+ *
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ * @return the scaled value at (x, z).
+ */
+ float getScaledHeightAtPoint(int x, int z);
+
+ /**
+ * <code>getSize</code> returns the size of one side the height map. Where
+ * the area of the height map is size x size.
+ *
+ * @return the size of a single side.
+ */
+ int getSize();
+
+ /**
+ * <code>getTrueHeightAtPoint</code> returns the non-scaled value at the
+ * point provided.
+ *
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ * @return the value at (x,z).
+ */
+ float getTrueHeightAtPoint(int x, int z);
+
+ /**
+ * <code>load</code> populates the height map data. This is dependent on
+ * the subclass's implementation.
+ *
+ * @return true if the load was successful, false otherwise.
+ */
+ boolean load();
+
+ /**
+ * <code>setHeightAtPoint</code> sets the height value for a given
+ * coordinate. It is recommended that the height value be within the 0 - 255
+ * range.
+ *
+ * @param height
+ * the new height for the coordinate.
+ * @param x
+ * the x (east/west) coordinate.
+ * @param z
+ * the z (north/south) coordinate.
+ */
+ void setHeightAtPoint(float height, int x, int z);
+
+ /**
+ * <code>setHeightScale</code> sets the scale of the height values.
+ * Typically, the height is a little too extreme and should be scaled to a
+ * smaller value (i.e. 0.25), to produce cleaner slopes.
+ *
+ * @param scale
+ * the scale to multiply height values by.
+ */
+ void setHeightScale(float scale);
+
+ /**
+ * <code>setFilter</code> sets the erosion value for the filter. This
+ * value must be between 0 and 1, where 0.2 - 0.4 produces arguably the best
+ * results.
+ *
+ * @param filter
+ * the erosion value.
+ * @throws Exception
+ * @throws JmeException
+ * if filter is less than 0 or greater than 1.
+ */
+ void setMagnificationFilter(float filter) throws Exception;
+
+ /**
+ * <code>setSize</code> sets the size of the terrain where the area is
+ * size x size.
+ *
+ * @param size
+ * the new size of the terrain.
+ * @throws Exception
+ *
+ * @throws JmeException
+ * if the size is less than or equal to zero.
+ */
+ void setSize(int size) throws Exception;
+
+ /**
+ * <code>unloadHeightMap</code> clears the data of the height map. This
+ * insures it is ready for reloading.
+ */
+ void unloadHeightMap();
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/HeightMapGrid.java b/engine/src/terrain/com/jme3/terrain/heightmap/HeightMapGrid.java
new file mode 100644
index 0000000..8d59429
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/HeightMapGrid.java
@@ -0,0 +1,23 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.math.Vector3f;
+
+/**
+ *
+ * @author Anthyon
+ */
+@Deprecated
+/**
+ * @Deprecated in favor of TerrainGridTileLoader
+ */
+public interface HeightMapGrid {
+
+ public HeightMap getHeightMapAt(Vector3f location);
+
+ public void setSize(int size);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/HillHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/HillHeightMap.java
new file mode 100644
index 0000000..038dcb2
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/HillHeightMap.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import java.util.Random;
+import java.util.logging.Logger;
+
+/**
+ * <code>HillHeightMap</code> generates a height map base on the Hill
+ * Algorithm. Terrain is generatd by growing hills of random size and height at
+ * random points in the heightmap. The terrain is then normalized and valleys
+ * can be flattened.
+ *
+ * @author Frederik Blthoff
+ * @see <a href="http://www.robot-frog.com/3d/hills/hill.html">Hill Algorithm</a>
+ */
+public class HillHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(HillHeightMap.class.getName());
+ private int iterations; // how many hills to generate
+ private float minRadius; // the minimum size of a hill radius
+ private float maxRadius; // the maximum size of a hill radius
+ private long seed; // the seed for the random number generator
+
+ /**
+ * Constructor sets the attributes of the hill system and generates the
+ * height map.
+ *
+ * @param size
+ * size the size of the terrain to be generated
+ * @param iterations
+ * the number of hills to grow
+ * @param minRadius
+ * the minimum radius of a hill
+ * @param maxRadius
+ * the maximum radius of a hill
+ * @param seed
+ * the seed to generate the same heightmap again
+ * @throws Exception
+ * @throws JmeException
+ * if size of the terrain is not greater that zero, or number of
+ * iterations is not greater that zero
+ */
+ public HillHeightMap(int size, int iterations, float minRadius,
+ float maxRadius, long seed) throws Exception {
+ if (size <= 0 || iterations <= 0 || minRadius <= 0 || maxRadius <= 0
+ || minRadius >= maxRadius) {
+ throw new Exception(
+ "Either size of the terrain is not greater that zero, "
+ + "or number of iterations is not greater that zero, "
+ + "or minimum or maximum radius are not greater than zero, "
+ + "or minimum radius is greater than maximum radius, "
+ + "or power of flattening is below one");
+ }
+ logger.info("Contructing hill heightmap using seed: " + seed);
+ this.size = size;
+ this.seed = seed;
+ this.iterations = iterations;
+ this.minRadius = minRadius;
+ this.maxRadius = maxRadius;
+
+ load();
+ }
+
+ /**
+ * Constructor sets the attributes of the hill system and generates the
+ * height map by using a random seed.
+ *
+ * @param size
+ * size the size of the terrain to be generated
+ * @param iterations
+ * the number of hills to grow
+ * @param minRadius
+ * the minimum radius of a hill
+ * @param maxRadius
+ * the maximum radius of a hill
+ * @throws Exception
+ * @throws JmeException
+ * if size of the terrain is not greater that zero, or number of
+ * iterations is not greater that zero
+ */
+ public HillHeightMap(int size, int iterations, float minRadius,
+ float maxRadius) throws Exception {
+ this(size, iterations, minRadius, maxRadius, new Random().nextLong());
+ }
+
+ /*
+ * Generates a heightmap using the Hill Algorithm and the attributes set by
+ * the constructor or the setters.
+ */
+ public boolean load() {
+ // clean up data if needed.
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+ heightData = new float[size * size];
+ float[][] tempBuffer = new float[size][size];
+ Random random = new Random(seed);
+
+ // Add the hills
+ for (int i = 0; i < iterations; i++) {
+ addHill(tempBuffer, random);
+ }
+
+ // transfer temporary buffer to final heightmap
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ setHeightAtPoint((float) tempBuffer[i][j], j, i);
+ }
+ }
+
+ normalizeTerrain(NORMALIZE_RANGE);
+
+ logger.info("Created Heightmap using the Hill Algorithm");
+
+ return true;
+ }
+
+ /**
+ * Generates a new hill of random size and height at a random position in
+ * the heightmap. This is the actual Hill algorithm. The <code>Random</code>
+ * object is used to guarantee the same heightmap for the same seed and
+ * attributes.
+ *
+ * @param tempBuffer
+ * the temporary height map buffer
+ * @param random
+ * the random number generator
+ */
+ protected void addHill(float[][] tempBuffer, Random random) {
+ // Pick the radius for the hill
+ float radius = randomRange(random, minRadius, maxRadius);
+
+ // Pick a centerpoint for the hill
+ float x = randomRange(random, -radius, size + radius);
+ float y = randomRange(random, -radius, size + radius);
+
+ float radiusSq = radius * radius;
+ float distSq;
+ float height;
+
+ // Find the range of hills affected by this hill
+ int xMin = Math.round(x - radius - 1);
+ int xMax = Math.round(x + radius + 1);
+
+ int yMin = Math.round(y - radius - 1);
+ int yMax = Math.round(y + radius + 1);
+
+ // Don't try to affect points outside the heightmap
+ if (xMin < 0) {
+ xMin = 0;
+ }
+ if (xMax > size) {
+ xMax = size - 1;
+ }
+
+ if (yMin < 0) {
+ yMin = 0;
+ }
+ if (yMax > size) {
+ yMax = size - 1;
+ }
+
+ for (int i = xMin; i <= xMax; i++) {
+ for (int j = yMin; j <= yMax; j++) {
+ distSq = (x - i) * (x - i) + (y - j) * (y - j);
+ height = radiusSq - distSq;
+
+ if (height > 0) {
+ tempBuffer[i][j] += height;
+ }
+ }
+ }
+ }
+
+ private float randomRange(Random random, float min, float max) {
+ return (random.nextInt() * (max - min) / Integer.MAX_VALUE) + min;
+ }
+
+ /**
+ * Sets the number of hills to grow. More hills usually mean a nicer
+ * heightmap.
+ *
+ * @param iterations
+ * the number of hills to grow
+ * @throws Exception
+ * @throws JmeException
+ * if iterations if not greater than zero
+ */
+ public void setIterations(int iterations) throws Exception {
+ if (iterations <= 0) {
+ throw new Exception(
+ "Number of iterations is not greater than zero");
+ }
+ this.iterations = iterations;
+ }
+
+ /**
+ * Sets the minimum radius of a hill.
+ *
+ * @param maxRadius
+ * the maximum radius of a hill
+ * @throws Exception
+ * @throws JmeException
+ * if the maximum radius if not greater than zero or not greater
+ * than the minimum radius
+ */
+ public void setMaxRadius(float maxRadius) throws Exception {
+ if (maxRadius <= 0 || maxRadius <= minRadius) {
+ throw new Exception("The maximum radius is not greater than 0, "
+ + "or not greater than the minimum radius");
+ }
+ this.maxRadius = maxRadius;
+ }
+
+ /**
+ * Sets the maximum radius of a hill.
+ *
+ * @param minRadius
+ * the minimum radius of a hill
+ * @throws Exception
+ * @throws JmeException if the minimum radius is not greater than zero or not
+ * lower than the maximum radius
+ */
+ public void setMinRadius(float minRadius) throws Exception {
+ if (minRadius <= 0 || minRadius >= maxRadius) {
+ throw new Exception("The minimum radius is not greater than 0, "
+ + "or not lower than the maximum radius");
+ }
+ this.minRadius = minRadius;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMap.java
new file mode 100644
index 0000000..84eb857
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMap.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.terrain.heightmap;
+
+import java.nio.ByteBuffer;
+import com.jme3.math.ColorRGBA;
+import com.jme3.texture.Image;
+import java.nio.ShortBuffer;
+
+/**
+ * <code>ImageBasedHeightMap</code> is a height map created from the grayscale
+ * conversion of an image. The image used currently must have an equal height
+ * and width, although future work could scale an incoming image to a specific
+ * height and width.
+ *
+ * @author Mike Kienenberger
+ * @version $id$
+ */
+public class ImageBasedHeightMap extends AbstractHeightMap implements ImageHeightmap {
+
+
+ protected Image colorImage;
+
+
+ public void setImage(Image image) {
+ this.colorImage = image;
+ }
+
+ /**
+ * Creates a HeightMap from an Image. The image will be converted to
+ * grayscale, and the grayscale values will be used to generate the height
+ * map. White is highest point while black is lowest point.
+ *
+ * Currently, the Image used must be square (width == height), but future
+ * work could rescale the image.
+ *
+ * @param colorImage
+ * Image to map to the height map.
+ */
+ public ImageBasedHeightMap(Image colorImage) {
+ this.colorImage = colorImage;
+ }
+
+ public ImageBasedHeightMap(Image colorImage, float heightScale) {
+ this.colorImage = colorImage;
+ this.heightScale = heightScale;
+ }
+
+ /**
+ * Loads the image data from top left to bottom right
+ */
+ public boolean load() {
+ return load(false, false);
+ }
+
+ /**
+ * Get the grayscale value, or override in your own sub-classes
+ */
+ protected float calculateHeight(float red, float green, float blue) {
+ return (float) (0.299 * red + 0.587 * green + 0.114 * blue);
+ }
+
+ public boolean load(boolean flipX, boolean flipY) {
+
+ int imageWidth = colorImage.getWidth();
+ int imageHeight = colorImage.getHeight();
+
+ if (imageWidth != imageHeight)
+ throw new RuntimeException("imageWidth: " + imageWidth
+ + " != imageHeight: " + imageHeight);
+
+ size = imageWidth;
+
+ ByteBuffer buf = colorImage.getData(0);
+
+ heightData = new float[(imageWidth * imageHeight)];
+
+ ColorRGBA colorStore = new ColorRGBA();
+
+ int index = 0;
+ if (flipY) {
+ for (int h = 0; h < imageHeight; ++h) {
+ if (flipX) {
+ for (int w = imageWidth - 1; w >= 0; --w) {
+ int baseIndex = (h * imageWidth)+ w;
+ heightData[index++] = getHeightAtPostion(buf, colorImage, baseIndex, colorStore)*heightScale;
+ }
+ } else {
+ for (int w = 0; w < imageWidth; ++w) {
+ int baseIndex = (h * imageWidth)+ w;
+ heightData[index++] = getHeightAtPostion(buf, colorImage, baseIndex, colorStore)*heightScale;
+ }
+ }
+ }
+ } else {
+ for (int h = imageHeight - 1; h >= 0; --h) {
+ if (flipX) {
+ for (int w = imageWidth - 1; w >= 0; --w) {
+ int baseIndex = (h * imageWidth)+ w;
+ heightData[index++] = getHeightAtPostion(buf, colorImage, baseIndex, colorStore)*heightScale;
+ }
+ } else {
+ for (int w = 0; w < imageWidth; ++w) {
+ int baseIndex = (h * imageWidth)+ w;
+ heightData[index++] = getHeightAtPostion(buf, colorImage, baseIndex, colorStore)*heightScale;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ protected float getHeightAtPostion(ByteBuffer buf, Image image, int position, ColorRGBA store) {
+ switch (image.getFormat()){
+ case RGBA8:
+ buf.position( position * 4 );
+ store.set(byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()));
+ return calculateHeight(store.r, store.g, store.b);
+ case ABGR8:
+ buf.position( position * 4 );
+ float a = byte2float(buf.get());
+ float b = byte2float(buf.get());
+ float g = byte2float(buf.get());
+ float r = byte2float(buf.get());
+ store.set(r,g,b,a);
+ return calculateHeight(store.r, store.g, store.b);
+ case RGB8:
+ buf.position( position * 3 );
+ store.set(byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()), 1);
+ return calculateHeight(store.r, store.g, store.b);
+ case Luminance8:
+ buf.position( position );
+ return byte2float(buf.get())*255*heightScale;
+ case Luminance16:
+ ShortBuffer sbuf = buf.asShortBuffer();
+ sbuf.position( position );
+ return (sbuf.get() & 0xFFFF) / 65535f * 255f * heightScale;
+ default:
+ throw new UnsupportedOperationException("Image format: "+image.getFormat());
+ }
+ }
+
+ private float byte2float(byte b){
+ return ((float)(b & 0xFF)) / 255f;
+ }
+} \ No newline at end of file
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMapGrid.java b/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMapGrid.java
new file mode 100644
index 0000000..1ba5f3e
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/ImageBasedHeightMapGrid.java
@@ -0,0 +1,79 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.AssetNotFoundException;
+import com.jme3.asset.TextureKey;
+import com.jme3.math.Vector3f;
+import com.jme3.texture.Texture;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Loads Terrain grid tiles with image heightmaps.
+ * By default it expects a 16-bit grayscale image as the heightmap, but
+ * you can also call setImageType(BufferedImage.TYPE_) to set it to be a different
+ * image type. If you do this, you must also set a custom ImageHeightmap that will
+ * understand and be able to parse the image. By default if you pass in an image of type
+ * BufferedImage.TYPE_3BYTE_BGR, it will use the ImageBasedHeightMap for you.
+ *
+ * @author Anthyon, Brent Owens
+ */
+@Deprecated
+/**
+ * @Deprecated in favor of ImageTileLoader
+ */
+public class ImageBasedHeightMapGrid implements HeightMapGrid {
+
+ private static final Logger logger = Logger.getLogger(ImageBasedHeightMapGrid.class.getName());
+ private final AssetManager assetManager;
+ private final Namer namer;
+ private int size;
+
+
+ public ImageBasedHeightMapGrid(final String textureBase, final String textureExt, AssetManager assetManager) {
+ this(assetManager, new Namer() {
+
+ public String getName(int x, int y) {
+ return textureBase + "_" + x + "_" + y + "." + textureExt;
+ }
+ });
+ }
+
+ public ImageBasedHeightMapGrid(AssetManager assetManager, Namer namer) {
+ this.assetManager = assetManager;
+ this.namer = namer;
+ }
+
+ public HeightMap getHeightMapAt(Vector3f location) {
+ // HEIGHTMAP image (for the terrain heightmap)
+ int x = (int) location.x;
+ int z = (int) location.z;
+
+ AbstractHeightMap heightmap = null;
+ //BufferedImage im = null;
+
+ try {
+ String name = namer.getName(x, z);
+ logger.log(Level.INFO, "Loading heightmap from file: {0}", name);
+ final Texture texture = assetManager.loadTexture(new TextureKey(name));
+
+ // CREATE HEIGHTMAP
+ heightmap = new ImageBasedHeightMap(texture.getImage());
+
+ heightmap.setHeightScale(1);
+ heightmap.load();
+
+ } catch (AssetNotFoundException e) {
+ logger.log(Level.SEVERE, "Asset Not found! ", e);
+ }
+ return heightmap;
+ }
+
+ public void setSize(int size) {
+ this.size = size - 1;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/ImageHeightmap.java b/engine/src/terrain/com/jme3/terrain/heightmap/ImageHeightmap.java
new file mode 100644
index 0000000..76a222b
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/ImageHeightmap.java
@@ -0,0 +1,30 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.texture.Image;
+
+/**
+ * A heightmap that is built off an image.
+ * If you want to be able to supply different Image types to
+ * ImageBaseHeightMapGrid, you need to implement this interface,
+ * and have that class extend Abstract heightmap.
+ *
+ * @author bowens
+ * @deprecated
+ */
+public interface ImageHeightmap {
+
+ /**
+ * Set the image to use for this heightmap
+ */
+ //public void setImage(Image image);
+
+ /**
+ * The BufferedImage.TYPE_ that is supported
+ * by this ImageHeightmap
+ */
+ //public int getSupportedImageType();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/MidpointDisplacementHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/MidpointDisplacementHeightMap.java
new file mode 100644
index 0000000..acb2b12
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/MidpointDisplacementHeightMap.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.math.FastMath;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.management.JMException;
+
+/**
+ * <code>MidpointDisplacementHeightMap</code> generates an heightmap based on
+ * the midpoint displacement algorithm. See Constructor javadoc for more info.
+ * @author cghislai
+ */
+public class MidpointDisplacementHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(MidpointDisplacementHeightMap.class.getName());
+ private float range; // The offset in which randomness will be added
+ private float persistence; // How the random offset evolves with increasing passes
+ private long seed; // seed for random number generator
+
+ /**
+ * The constructor generates the heightmap. After the first 4 corners are
+ * randomly given an height, the center will be heighted to the average of
+ * the 4 corners to which a random value is added. Then other passes fill
+ * the heightmap by the same principle.
+ * The random value is generated between the values <code>-range</code>
+ * and <code>range</code>. The <code>range</code> parameter is multiplied by
+ * the <code>persistence</code> parameter each pass to smoothen close cell heights.
+ * Extends this class and override the getOffset function for more control of
+ * the randomness (you can use the coordinates and/or the computed average height
+ * to influence the random amount added.
+ *
+ * @param size
+ * The size of the heightmap, must be 2^N+1
+ * @param range
+ * The range in which randomness will be added. A value of 1 will
+ * allow -1 to 1 value changes.
+ * @param persistence
+ * The factor by which the range will evolve at each iteration.
+ * A value of 0.5f will halve the range at each iteration and is
+ * typically a good choice
+ * @param seed
+ * A seed to feed the random number generator.
+ * @throw JMException if size is not a power of two plus one.
+ */
+ public MidpointDisplacementHeightMap(int size, float range, float persistence, long seed) throws Exception {
+ if (size < 0 || !FastMath.isPowerOfTwo(size - 1)) {
+ throw new JMException("The size is negative or not of the form 2^N +1"
+ + " (a power of two plus one)");
+ }
+ this.size = size;
+ this.range = range;
+ this.persistence = persistence;
+ this.seed = seed;
+ load();
+ }
+
+ /**
+ * The constructor generates the heightmap. After the first 4 corners are
+ * randomly given an height, the center will be heighted to the average of
+ * the 4 corners to which a random value is added. Then other passes fill
+ * the heightmap by the same principle.
+ * The random value is generated between the values <code>-range</code>
+ * and <code>range</code>. The <code>range</code> parameter is multiplied by
+ * the <code>persistence</code> parameter each pass to smoothen close cell heights.
+ * @param size
+ * The size of the heightmap, must be 2^N+1
+ * @param range
+ * The range in which randomness will be added. A value of 1 will
+ * allow -1 to 1 value changes.
+ * @param persistence
+ * The factor by which the range will evolve at each iteration.
+ * A value of 0.5f will halve the range at each iteration and is
+ * typically a good choice
+ * @throw JMException if size is not a power of two plus one.
+ */
+ public MidpointDisplacementHeightMap(int size, float range, float persistence) throws Exception {
+ this(size, range, persistence, new Random().nextLong());
+ }
+
+ /**
+ * Generate the heightmap.
+ * @return
+ */
+ @Override
+ public boolean load() {
+ // clean up data if needed.
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+ heightData = new float[size * size];
+ float[][] tempBuffer = new float[size][size];
+ Random random = new Random(seed);
+
+ tempBuffer[0][0] = random.nextFloat();
+ tempBuffer[0][size - 1] = random.nextFloat();
+ tempBuffer[size - 1][0] = random.nextFloat();
+ tempBuffer[size - 1][size - 1] = random.nextFloat();
+
+ float offsetRange = range;
+ int stepSize = size - 1;
+ while (stepSize > 1) {
+ int[] nextCoords = {0, 0};
+ while (nextCoords != null) {
+ nextCoords = doSquareStep(tempBuffer, nextCoords, stepSize, offsetRange, random);
+ }
+ nextCoords = new int[]{0, 0};
+ while (nextCoords != null) {
+ nextCoords = doDiamondStep(tempBuffer, nextCoords, stepSize, offsetRange, random);
+ }
+ stepSize /= 2;
+ offsetRange *= persistence;
+ }
+
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ setHeightAtPoint((float) tempBuffer[i][j], j, i);
+ }
+ }
+
+ normalizeTerrain(NORMALIZE_RANGE);
+
+ logger.log(Level.INFO, "Midpoint displacement heightmap generated");
+ return true;
+ }
+
+ /**
+ * Will fill the value at (coords[0]+stepSize/2, coords[1]+stepSize/2) with
+ * the average from the corners of the square with topleft corner at (coords[0],coords[1])
+ * and width of stepSize.
+ * @param tempBuffer the temprary heightmap
+ * @param coords an int array of lenght 2 with the x coord in position 0
+ * @param stepSize the size of the square
+ * @param offsetRange the offset range within a random value is picked and added to the average
+ * @param random the random generator
+ * @return
+ */
+ protected int[] doSquareStep(float[][] tempBuffer, int[] coords, int stepSize, float offsetRange, Random random) {
+ float cornerAverage = 0;
+ int x = coords[0];
+ int y = coords[1];
+ cornerAverage += tempBuffer[x][y];
+ cornerAverage += tempBuffer[x + stepSize][y];
+ cornerAverage += tempBuffer[x + stepSize][y + stepSize];
+ cornerAverage += tempBuffer[x][y + stepSize];
+ cornerAverage /= 4;
+ float offset = getOffset(random, offsetRange, coords, cornerAverage);
+ tempBuffer[x + stepSize / 2][y + stepSize / 2] = cornerAverage + offset;
+
+ // Only get to next square if the center is still in map
+ if (x + stepSize * 3 / 2 < size) {
+ return new int[]{x + stepSize, y};
+ }
+ if (y + stepSize * 3 / 2 < size) {
+ return new int[]{0, y + stepSize};
+ }
+ return null;
+ }
+
+ /**
+ * Will fill the cell at (x+stepSize/2, y) with the average of the 4 corners
+ * of the diamond centered on that point with width and height of stepSize.
+ * @param tempBuffer
+ * @param coords
+ * @param stepSize
+ * @param offsetRange
+ * @param random
+ * @return
+ */
+ protected int[] doDiamondStep(float[][] tempBuffer, int[] coords, int stepSize, float offsetRange, Random random) {
+ int cornerNbr = 0;
+ float cornerAverage = 0;
+ int x = coords[0];
+ int y = coords[1];
+ int[] dxs = new int[]{0, stepSize / 2, stepSize, stepSize / 2};
+ int[] dys = new int[]{0, -stepSize / 2, 0, stepSize / 2};
+
+ for (int d = 0; d < 4; d++) {
+ int i = x + dxs[d];
+ if (i < 0 || i > size - 1) {
+ continue;
+ }
+ int j = y + dys[d];
+ if (j < 0 || j > size - 1) {
+ continue;
+ }
+ cornerAverage += tempBuffer[i][j];
+ cornerNbr++;
+ }
+ cornerAverage /= cornerNbr;
+ float offset = getOffset(random, offsetRange, coords, cornerAverage);
+ tempBuffer[x + stepSize / 2][y] = cornerAverage + offset;
+
+ if (x + stepSize * 3 / 2 < size) {
+ return new int[]{x + stepSize, y};
+ }
+ if (y + stepSize / 2 < size) {
+ if (x + stepSize == size - 1) {
+ return new int[]{-stepSize / 2, y + stepSize / 2};
+ } else {
+ return new int[]{0, y + stepSize / 2};
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generate a random value to add to the computed average
+ * @param random the random generator
+ * @param offsetRange
+ * @param coords
+ * @param average
+ * @return A semi-random value within offsetRange
+ */
+ protected float getOffset(Random random, float offsetRange, int[] coords, float average) {
+ return 2 * (random.nextFloat() - 0.5F) * offsetRange;
+ }
+
+ public float getPersistence() {
+ return persistence;
+ }
+
+ public void setPersistence(float persistence) {
+ this.persistence = persistence;
+ }
+
+ public float getRange() {
+ return range;
+ }
+
+ public void setRange(float range) {
+ this.range = range;
+ }
+
+ public long getSeed() {
+ return seed;
+ }
+
+ public void setSeed(long seed) {
+ this.seed = seed;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/Namer.java b/engine/src/terrain/com/jme3/terrain/heightmap/Namer.java
new file mode 100644
index 0000000..73171ae
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/Namer.java
@@ -0,0 +1,21 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.terrain.heightmap;
+
+/**
+ *
+ * @author Anthyon
+ */
+public interface Namer {
+
+ /**
+ * Gets a name for a heightmap tile given it's cell id
+ * @param x
+ * @param y
+ * @return
+ */
+ public String getName(int x, int y);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/ParticleDepositionHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/ParticleDepositionHeightMap.java
new file mode 100644
index 0000000..63dccc6
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/ParticleDepositionHeightMap.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import java.util.logging.Logger;
+
+/**
+ * <code>ParticleDepositionHeightMap</code> creates a heightmap based on the
+ * Particle Deposition algorithm based on Jason Shankel's paper from
+ * "Game Programming Gems". A heightmap is created using a Molecular beam
+ * epitaxy, or MBE, for depositing thin layers of atoms on a substrate.
+ * We drop a sequence of particles and simulate their flow across a surface
+ * of previously dropped particles. This creates a few high peaks, for further
+ * realism we can define a caldera. Similar to the way volcano's form
+ * islands, rock is deposited via lava, when the lava cools, it recedes
+ * into the volcano, creating the caldera.
+ *
+ * @author Mark Powell
+ * @version $Id$
+ */
+public class ParticleDepositionHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(ParticleDepositionHeightMap.class.getName());
+ //Attributes.
+ private int jumps;
+ private int peakWalk;
+ private int minParticles;
+ private int maxParticles;
+ private float caldera;
+
+ /**
+ * Constructor sets the attributes of the Particle Deposition
+ * Height Map and then generates the map.
+ *
+ * @param size the size of the terrain where the area is size x size.
+ * @param jumps number of areas to drop particles. Can also think
+ * of it as the number of peaks.
+ * @param peakWalk determines how much to agitate the drop point
+ * during a creation of a single peak. The lower the number
+ * the more the drop point will be agitated. 1 will insure
+ * agitation every round.
+ * @param minParticles defines the minimum number of particles to
+ * drop during a single jump.
+ * @param maxParticles defines the maximum number of particles to
+ * drop during a single jump.
+ * @param caldera defines the altitude to invert a peak. This is
+ * represented as a percentage, where 0.0 will not invert
+ * anything, and 1.0 will invert all.
+ *
+ * @throws JmeException if any value is less than zero, and
+ * if caldera is not between 0 and 1. If minParticles is greater than
+ * max particles as well.
+ */
+ public ParticleDepositionHeightMap(
+ int size,
+ int jumps,
+ int peakWalk,
+ int minParticles,
+ int maxParticles,
+ float caldera) throws Exception {
+
+
+ if (size <= 0
+ || jumps < 0
+ || peakWalk < 0
+ || minParticles > maxParticles
+ || minParticles < 0
+ || maxParticles < 0) {
+
+
+ throw new Exception(
+ "values must be greater than zero, "
+ + "and minParticles must be greater than maxParticles");
+ }
+ if (caldera < 0.0f || caldera > 1.0f) {
+ throw new Exception(
+ "Caldera level must be " + "between 0 and 1");
+ }
+
+
+ this.size = size;
+ this.jumps = jumps;
+ this.peakWalk = peakWalk;
+ this.minParticles = minParticles;
+ this.maxParticles = maxParticles;
+ this.caldera = caldera;
+
+
+ load();
+ }
+
+ /**
+ * <code>load</code> generates the heightfield using the Particle Deposition
+ * algorithm. <code>load</code> uses the latest attributes, so a call
+ * to <code>load</code> is recommended if attributes have changed using
+ * the set methods.
+ */
+ public boolean load() {
+ int x, y;
+ int calderaX, calderaY;
+ int sx, sy;
+ int tx, ty;
+ int m;
+ float calderaStartPoint;
+ float cutoff;
+ int dx[] = {0, 1, 0, size - 1, 1, 1, size - 1, size - 1};
+ int dy[] = {1, 0, size - 1, 0, size - 1, 1, size - 1, 1};
+ float[][] tempBuffer = new float[size][size];
+ //map 0 unmarked, unvisited, 1 marked, unvisited, 2 marked visited.
+ int[][] calderaMap = new int[size][size];
+ boolean done;
+
+
+ int minx, maxx;
+ int miny, maxy;
+
+
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+
+
+ heightData = new float[size * size];
+
+
+ //create peaks.
+ for (int i = 0; i < jumps; i++) {
+
+
+ //pick a random point.
+ x = (int) (Math.rint(Math.random() * (size - 1)));
+ y = (int) (Math.rint(Math.random() * (size - 1)));
+
+
+ //set the caldera point.
+ calderaX = x;
+ calderaY = y;
+
+
+ int numberParticles =
+ (int) (Math.rint(
+ (Math.random() * (maxParticles - minParticles))
+ + minParticles));
+ //drop particles.
+ for (int j = 0; j < numberParticles; j++) {
+ //check to see if we should aggitate the drop point.
+ if (peakWalk != 0 && j % peakWalk == 0) {
+ m = (int) (Math.rint(Math.random() * 7));
+ x = (x + dx[m] + size) % size;
+ y = (y + dy[m] + size) % size;
+ }
+
+
+ //add the particle to the piont.
+ tempBuffer[x][y] += 1;
+
+
+ sx = x;
+ sy = y;
+ done = false;
+
+
+ //cause the particle to "slide" down the slope and settle at
+ //a low point.
+ while (!done) {
+ done = true;
+
+
+ //check neighbors to see if we are higher.
+ m = (int) (Math.rint((Math.random() * 8)));
+ for (int jj = 0; jj < 8; jj++) {
+ tx = (sx + dx[(jj + m) % 8]) % (size);
+ ty = (sy + dy[(jj + m) % 8]) % (size);
+
+
+ //move to the neighbor.
+ if (tempBuffer[tx][ty] + 1.0f < tempBuffer[sx][sy]) {
+ tempBuffer[tx][ty] += 1.0f;
+ tempBuffer[sx][sy] -= 1.0f;
+ sx = tx;
+ sy = ty;
+ done = false;
+ break;
+ }
+ }
+ }
+
+
+ //This point is higher than the current caldera point,
+ //so move the caldera here.
+ if (tempBuffer[sx][sy] > tempBuffer[calderaX][calderaY]) {
+ calderaX = sx;
+ calderaY = sy;
+ }
+ }
+
+
+ //apply the caldera.
+ calderaStartPoint = tempBuffer[calderaX][calderaY];
+ cutoff = calderaStartPoint * (1.0f - caldera);
+ minx = calderaX;
+ maxx = calderaX;
+ miny = calderaY;
+ maxy = calderaY;
+
+
+ calderaMap[calderaX][calderaY] = 1;
+
+
+ done = false;
+ while (!done) {
+ done = true;
+ sx = minx;
+ sy = miny;
+ tx = maxx;
+ ty = maxy;
+
+
+ for (x = sx; x <= tx; x++) {
+ for (y = sy; y <= ty; y++) {
+
+
+ calderaX = (x + size) % size;
+ calderaY = (y + size) % size;
+
+
+ if (calderaMap[calderaX][calderaY] == 1) {
+ calderaMap[calderaX][calderaY] = 2;
+
+
+ if (tempBuffer[calderaX][calderaY] > cutoff
+ && tempBuffer[calderaX][calderaY]
+ <= calderaStartPoint) {
+
+
+ done = false;
+ tempBuffer[calderaX][calderaY] =
+ 2 * cutoff - tempBuffer[calderaX][calderaY];
+
+
+ //check the left and right neighbors
+ calderaX = (calderaX + 1) % size;
+ if (calderaMap[calderaX][calderaY] == 0) {
+ if (x + 1 > maxx) {
+ maxx = x + 1;
+ }
+ calderaMap[calderaX][calderaY] = 1;
+ }
+
+
+ calderaX = (calderaX + size - 2) % size;
+ if (calderaMap[calderaX][calderaY] == 0) {
+ if (x - 1 < minx) {
+ minx = x - 1;
+ }
+ calderaMap[calderaX][calderaY] = 1;
+ }
+
+
+ //check the upper and lower neighbors.
+ calderaX = (x + size) % size;
+ calderaY = (calderaY + 1) % size;
+ if (calderaMap[calderaX][calderaY] == 0) {
+ if (y + 1 > maxy) {
+ maxy = y + 1;
+ }
+ calderaMap[calderaX][calderaY] = 1;
+ }
+ calderaY = (calderaY + size - 2) % size;
+ if (calderaMap[calderaX][calderaY] == 0) {
+ if (y - 1 < miny) {
+ miny = y - 1;
+ }
+ calderaMap[calderaX][calderaY] = 1;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ //transfer the new terrain into the height map.
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ setHeightAtPoint((float) tempBuffer[i][j], j, i);
+ }
+ }
+ erodeTerrain();
+ normalizeTerrain(NORMALIZE_RANGE);
+
+ logger.info("Created heightmap using Particle Deposition");
+
+
+ return false;
+ }
+
+ /**
+ * <code>setJumps</code> sets the number of jumps or peaks that will
+ * be created during the next call to <code>load</code>.
+ * @param jumps the number of jumps to use for next load.
+ * @throws JmeException if jumps is less than zero.
+ */
+ public void setJumps(int jumps) throws Exception {
+ if (jumps < 0) {
+ throw new Exception("jumps must be positive");
+ }
+ this.jumps = jumps;
+ }
+
+ /**
+ * <code>setPeakWalk</code> sets how often the jump point will be
+ * aggitated. The lower the peakWalk, the more often the point will
+ * be aggitated.
+ *
+ * @param peakWalk the amount to aggitate the jump point.
+ * @throws JmeException if peakWalk is negative or zero.
+ */
+ public void setPeakWalk(int peakWalk) throws Exception {
+ if (peakWalk <= 0) {
+ throw new Exception(
+ "peakWalk must be greater than " + "zero");
+ }
+ this.peakWalk = peakWalk;
+ }
+
+ /**
+ * <code>setCaldera</code> sets the level at which a peak will be
+ * inverted.
+ *
+ * @param caldera the level at which a peak will be inverted. This must be
+ * between 0 and 1, as it is represented as a percentage.
+ * @throws JmeException if caldera is not between 0 and 1.
+ */
+ public void setCaldera(float caldera) throws Exception {
+ if (caldera < 0.0f || caldera > 1.0f) {
+ throw new Exception(
+ "Caldera level must be " + "between 0 and 1");
+ }
+ this.caldera = caldera;
+ }
+
+ /**
+ * <code>setMaxParticles</code> sets the maximum number of particles
+ * for a single jump.
+ * @param maxParticles the maximum number of particles for a single jump.
+ * @throws JmeException if maxParticles is negative or less than
+ * the current number of minParticles.
+ */
+ public void setMaxParticles(int maxParticles) {
+ this.maxParticles = maxParticles;
+ }
+
+ /**
+ * <code>setMinParticles</code> sets the minimum number of particles
+ * for a single jump.
+ * @param minParticles the minimum number of particles for a single jump.
+ * @throws JmeException if minParticles are greater than
+ * the current maxParticles;
+ */
+ public void setMinParticles(int minParticles) throws Exception {
+ if (minParticles > maxParticles) {
+ throw new Exception(
+ "minParticles must be less " + "than the current maxParticles");
+ }
+ this.minParticles = minParticles;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/heightmap/RawHeightMap.java b/engine/src/terrain/com/jme3/terrain/heightmap/RawHeightMap.java
new file mode 100644
index 0000000..0318961
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/heightmap/RawHeightMap.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.terrain.heightmap;
+
+import com.jme3.math.FastMath;
+import com.jme3.util.LittleEndien;
+import java.io.*;
+import java.net.URL;
+import java.util.logging.Logger;
+
+/**
+ * <code>RawHeightMap</code> creates a height map from a RAW image file. The
+ * greyscale image denotes height based on the value of the pixel for each
+ * point. Where pure black the lowest point and pure white denotes the highest.
+ *
+ * @author Mark Powell
+ * @version $Id$
+ */
+public class RawHeightMap extends AbstractHeightMap {
+
+ private static final Logger logger = Logger.getLogger(RawHeightMap.class.getName());
+ /**
+ * Format specification for 8 bit precision heightmaps
+ */
+ public static final int FORMAT_8BIT = 0;
+ /**
+ * Format specification for 16 bit little endian heightmaps
+ */
+ public static final int FORMAT_16BITLE = 1;
+ /**
+ * Format specification for 16 bit big endian heightmaps
+ */
+ public static final int FORMAT_16BITBE = 2;
+ private int format;
+ private boolean swapxy;
+ private InputStream stream;
+
+ /**
+ * Constructor creates a new <code>RawHeightMap</code> object and loads a
+ * RAW image file to use as a height field. The greyscale image denotes the
+ * height of the terrain, where dark is low point and bright is high point.
+ * The values of the RAW correspond directly with the RAW values or 0 - 255.
+ *
+ * @param filename
+ * the RAW file to use as the heightmap.
+ * @param size
+ * the size of the RAW (must be square).
+ * @throws JmeException
+ * if the filename is null or not RAW, and if the size is 0 or
+ * less.
+ */
+ public RawHeightMap(String filename, int size) throws Exception {
+ this(filename, size, FORMAT_8BIT, false);
+ }
+
+ public RawHeightMap(float heightData[]) {
+ this.heightData = heightData;
+ this.size = (int) FastMath.sqrt(heightData.length);
+ this.format = FORMAT_8BIT;
+ }
+
+ public RawHeightMap(String filename, int size, int format, boolean swapxy) throws Exception {
+ // varify that filename and size are valid.
+ if (null == filename || size <= 0) {
+ throw new Exception("Must supply valid filename and "
+ + "size (> 0)");
+ }
+ try {
+ setup(new FileInputStream(filename), size, format, swapxy);
+ } catch (FileNotFoundException e) {
+ throw new Exception("height file not found: " + filename);
+ }
+ }
+
+ public RawHeightMap(InputStream stream, int size, int format, boolean swapxy) throws Exception {
+ setup(stream, size, format, swapxy);
+ }
+
+ public RawHeightMap(URL resource, int size, int format, boolean swapxy) throws Exception {
+ // varify that resource and size are valid.
+ if (null == resource || size <= 0) {
+ throw new Exception("Must supply valid resource and "
+ + "size (> 0)");
+ }
+
+
+ try {
+ setup(resource.openStream(), size, format, swapxy);
+ } catch (IOException e) {
+ throw new Exception("Unable to open height url: " + resource);
+ }
+ }
+
+ private void setup(InputStream stream, int size, int format, boolean swapxy) throws Exception {
+ // varify that filename and size are valid.
+ if (null == stream || size <= 0) {
+ throw new Exception("Must supply valid stream and "
+ + "size (> 0)");
+ }
+
+
+ this.stream = stream;
+ this.size = size;
+ this.format = format;
+ this.swapxy = swapxy;
+ load();
+ }
+
+ /**
+ * <code>load</code> fills the height data array with the appropriate data
+ * from the set RAW image. If the RAW image has not been set a JmeException
+ * will be thrown.
+ *
+ * @return true if the load is successfull, false otherwise.
+ */
+ @Override
+ public boolean load() {
+ // confirm data has been set. Redundant check...
+ if (null == stream || size <= 0) {
+ throw new RuntimeException("Must supply valid stream and "
+ + "size (> 0)");
+ }
+
+
+ // clean up
+ if (null != heightData) {
+ unloadHeightMap();
+ }
+
+
+ // initialize the height data attributes
+ heightData = new float[size * size];
+
+
+ // attempt to connect to the supplied file.
+ BufferedInputStream bis = null;
+
+
+ try {
+ bis = new BufferedInputStream(stream);
+ if (format == RawHeightMap.FORMAT_16BITLE) {
+ LittleEndien dis = new LittleEndien(bis);
+ int index;
+ // read the raw file
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ if (swapxy) {
+ index = i + j * size;
+ } else {
+ index = (i * size) + j;
+ }
+ heightData[index] = dis.readUnsignedShort();
+ }
+ }
+ dis.close();
+ } else {
+ DataInputStream dis = new DataInputStream(bis);
+ // read the raw file
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ int index;
+ if (swapxy) {
+ index = i + j * size;
+ } else {
+ index = (i * size) + j;
+ }
+ if (format == RawHeightMap.FORMAT_16BITBE) {
+ heightData[index] = dis.readUnsignedShort();
+ } else {
+ heightData[index] = dis.readUnsignedByte();
+ }
+ }
+ }
+ dis.close();
+ }
+ bis.close();
+ } catch (IOException e1) {
+ logger.warning("Error reading height data from stream.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * <code>setFilename</code> sets the file to use for the RAW data. A call
+ * to <code>load</code> is required to put the changes into effect.
+ *
+ * @param filename
+ * the new file to use for the height data.
+ * @throws JmeException
+ * if the file is null or not RAW.
+ */
+ public void setFilename(String filename) throws Exception {
+ if (null == filename) {
+ throw new Exception("Must supply valid filename.");
+ }
+ try {
+ this.stream = new FileInputStream(filename);
+ } catch (FileNotFoundException e) {
+ throw new Exception("height file not found: " + filename);
+ }
+ }
+
+ /**
+ * <code>setHeightStream</code> sets the stream to use for the RAW data. A call
+ * to <code>load</code> is required to put the changes into effect.
+ *
+ * @param stream
+ * the new stream to use for the height data.
+ * @throws JmeException
+ * if the stream is null or not RAW.
+ */
+ public void setHeightStream(InputStream stream) throws Exception {
+ if (null == stream) {
+ throw new Exception("Must supply valid stream.");
+ }
+ this.stream = stream;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/Basis.java b/engine/src/terrain/com/jme3/terrain/noise/Basis.java
new file mode 100644
index 0000000..9f310f6
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/Basis.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise;
+
+import java.nio.FloatBuffer;
+
+import com.jme3.terrain.noise.basis.ImprovedNoise;
+import com.jme3.terrain.noise.modulator.Modulator;
+
+/**
+ * Interface for - basically 3D - noise generation algorithms, based on the
+ * book: Texturing & Modeling - A Procedural Approach
+ *
+ * The main concept is to look at noise as a basis for generating fractals.
+ * Basis can be anything, like a simple:
+ *
+ * <code>
+ * float value(float x, float y, float z) {
+ * return 0; // a flat noise with 0 value everywhere
+ * }
+ * </code>
+ *
+ * or a more complex perlin noise ({@link ImprovedNoise}
+ *
+ * Fractals use these functions to generate a more complex result based on some
+ * frequency, roughness, etc values.
+ *
+ * Fractals themselves are implementing the Basis interface as well, opening
+ * an infinite range of results.
+ *
+ * @author Anthyon
+ *
+ * @since 2011
+ *
+ */
+public interface Basis {
+
+ public void init();
+
+ public Basis setScale(float scale);
+
+ public float getScale();
+
+ public Basis addModulator(Modulator modulator);
+
+ public float value(float x, float y, float z);
+
+ public FloatBuffer getBuffer(float sx, float sy, float base, int size);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/Color.java b/engine/src/terrain/com/jme3/terrain/noise/Color.java
new file mode 100644
index 0000000..719c0ff
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/Color.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise;
+
+/**
+ * Helper class for working with colors and gradients
+ *
+ * @author Anthyon
+ *
+ */
+public class Color {
+
+ private final float[] rgba = new float[4];
+
+ public Color() {}
+
+ public Color(final int r, final int g, final int b) {
+ this(r, g, b, 255);
+ }
+
+ public Color(final int r, final int g, final int b, final int a) {
+ this.rgba[0] = (r & 255) / 256f;
+ this.rgba[1] = (g & 255) / 256f;
+ this.rgba[2] = (b & 255) / 256f;
+ this.rgba[3] = (a & 255) / 256f;
+ }
+
+ public Color(final float r, final float g, final float b) {
+ this(r, g, b, 1);
+ }
+
+ public Color(final float r, final float g, final float b, final float a) {
+ this.rgba[0] = ShaderUtils.clamp(r, 0, 1);
+ this.rgba[1] = ShaderUtils.clamp(g, 0, 1);
+ this.rgba[2] = ShaderUtils.clamp(b, 0, 1);
+ this.rgba[3] = ShaderUtils.clamp(a, 0, 1);
+ }
+
+ public Color(final int h, final float s, final float b) {
+ this(h, s, b, 1);
+ }
+
+ public Color(final int h, final float s, final float b, final float a) {
+ this.rgba[3] = a;
+ if (s == 0) {
+ // achromatic ( grey )
+ this.rgba[0] = b;
+ this.rgba[1] = b;
+ this.rgba[2] = b;
+ return;
+ }
+
+ float hh = h / 60.0f;
+ int i = ShaderUtils.floor(hh);
+ float f = hh - i;
+ float p = b * (1 - s);
+ float q = b * (1 - s * f);
+ float t = b * (1 - s * (1 - f));
+
+ if (i == 0) {
+ this.rgba[0] = b;
+ this.rgba[1] = t;
+ this.rgba[2] = p;
+ } else if (i == 1) {
+ this.rgba[0] = q;
+ this.rgba[1] = b;
+ this.rgba[2] = p;
+ } else if (i == 2) {
+ this.rgba[0] = p;
+ this.rgba[1] = b;
+ this.rgba[2] = t;
+ } else if (i == 3) {
+ this.rgba[0] = p;
+ this.rgba[1] = q;
+ this.rgba[2] = b;
+ } else if (i == 4) {
+ this.rgba[0] = t;
+ this.rgba[1] = p;
+ this.rgba[2] = b;
+ } else {
+ this.rgba[0] = b;
+ this.rgba[1] = p;
+ this.rgba[2] = q;
+ }
+ }
+
+ public int toInteger() {
+ return 0x00000000 | (int) (this.rgba[3] * 256) << 24 | (int) (this.rgba[0] * 256) << 16 | (int) (this.rgba[1] * 256) << 8
+ | (int) (this.rgba[2] * 256);
+ }
+
+ public String toWeb() {
+ return Integer.toHexString(this.toInteger());
+ }
+
+ public Color toGrayscale() {
+ float v = (this.rgba[0] + this.rgba[1] + this.rgba[2]) / 3f;
+ return new Color(v, v, v, this.rgba[3]);
+ }
+
+ public Color toSepia() {
+ float r = ShaderUtils.clamp(this.rgba[0] * 0.393f + this.rgba[1] * 0.769f + this.rgba[2] * 0.189f, 0, 1);
+ float g = ShaderUtils.clamp(this.rgba[0] * 0.349f + this.rgba[1] * 0.686f + this.rgba[2] * 0.168f, 0, 1);
+ float b = ShaderUtils.clamp(this.rgba[0] * 0.272f + this.rgba[1] * 0.534f + this.rgba[2] * 0.131f, 0, 1);
+ return new Color(r, g, b, this.rgba[3]);
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/Filter.java b/engine/src/terrain/com/jme3/terrain/noise/Filter.java
new file mode 100644
index 0000000..1dca8bf
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/Filter.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise;
+
+import java.nio.FloatBuffer;
+
+public interface Filter {
+ public Filter addPreFilter(Filter filter);
+
+ public Filter addPostFilter(Filter filter);
+
+ public FloatBuffer doFilter(float sx, float sy, float base, FloatBuffer data, int size);
+
+ public int getMargin(int size, int margin);
+
+ public boolean isEnabled();
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/ShaderUtils.java b/engine/src/terrain/com/jme3/terrain/noise/ShaderUtils.java
new file mode 100644
index 0000000..d1c2f6e
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/ShaderUtils.java
@@ -0,0 +1,288 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Helper class containing useful functions explained in the book:
+ * Texturing & Modeling - A Procedural Approach
+ *
+ * @author Anthyon
+ *
+ */
+public class ShaderUtils {
+
+ public static final float[] i2c(final int color) {
+ return new float[] { (color & 0x00ff0000) / 256f, (color & 0x0000ff00) / 256f, (color & 0x000000ff) / 256f,
+ (color & 0xff000000) / 256f };
+ }
+
+ public static final int c2i(final float[] color) {
+ return (color.length == 4 ? (int) (color[3] * 256) : 0xff000000) | ((int) (color[0] * 256) << 16) | ((int) (color[1] * 256) << 8)
+ | (int) (color[2] * 256);
+ }
+
+ public static final float mix(final float a, final float b, final float f) {
+ return (1 - f) * a + f * b;
+ }
+
+ public static final Color mix(final Color a, final Color b, final float f) {
+ return new Color((int) ShaderUtils.clamp(ShaderUtils.mix(a.getRed(), b.getRed(), f), 0, 255), (int) ShaderUtils.clamp(
+ ShaderUtils.mix(a.getGreen(), b.getGreen(), f), 0, 255), (int) ShaderUtils.clamp(
+ ShaderUtils.mix(a.getBlue(), b.getBlue(), f), 0, 255));
+ }
+
+ public static final int mix(final int a, final int b, final float f) {
+ return (int) ((1 - f) * a + f * b);
+ }
+
+ public static final float[] mix(final float[] c1, final float[] c2, final float f) {
+ return new float[] { ShaderUtils.mix(c1[0], c2[0], f), ShaderUtils.mix(c1[1], c2[1], f), ShaderUtils.mix(c1[2], c2[2], f) };
+ }
+
+ public static final float step(final float a, final float x) {
+ return x < a ? 0 : 1;
+ }
+
+ public static final float boxstep(final float a, final float b, final float x) {
+ return ShaderUtils.clamp((x - a) / (b - a), 0, 1);
+ }
+
+ public static final float pulse(final float a, final float b, final float x) {
+ return ShaderUtils.step(a, x) - ShaderUtils.step(b, x);
+ }
+
+ public static final float clamp(final float x, final float a, final float b) {
+ return x < a ? a : x > b ? b : x;
+ }
+
+ public static final float min(final float a, final float b) {
+ return a < b ? a : b;
+ }
+
+ public static final float max(final float a, final float b) {
+ return a > b ? a : b;
+ }
+
+ public static final float abs(final float x) {
+ return x < 0 ? -x : x;
+ }
+
+ public static final float smoothstep(final float a, final float b, final float x) {
+ if (x < a) {
+ return 0;
+ } else if (x > b) {
+ return 1;
+ }
+ float xx = (x - a) / (b - a);
+ return xx * xx * (3 - 2 * xx);
+ }
+
+ public static final float mod(final float a, final float b) {
+ int n = (int) (a / b);
+ float aa = a - n * b;
+ if (aa < 0) {
+ aa += b;
+ }
+ return aa;
+ }
+
+ public static final int floor(final float x) {
+ return x > 0 ? (int) x : (int) x - 1;
+ }
+
+ public static final float ceil(final float x) {
+ return (int) x + (x > 0 && x != (int) x ? 1 : 0);
+ }
+
+ public static final float spline(float x, final float[] knot) {
+ float CR00 = -0.5f;
+ float CR01 = 1.5f;
+ float CR02 = -1.5f;
+ float CR03 = 0.5f;
+ float CR10 = 1.0f;
+ float CR11 = -2.5f;
+ float CR12 = 2.0f;
+ float CR13 = -0.5f;
+ float CR20 = -0.5f;
+ float CR21 = 0.0f;
+ float CR22 = 0.5f;
+ float CR23 = 0.0f;
+ float CR30 = 0.0f;
+ float CR31 = 1.0f;
+ float CR32 = 0.0f;
+ float CR33 = 0.0f;
+
+ int span;
+ int nspans = knot.length - 3;
+ float c0, c1, c2, c3; /* coefficients of the cubic. */
+ if (nspans < 1) {/* illegal */
+ throw new RuntimeException("Spline has too few knots.");
+ }
+ /* Find the appropriate 4-point span of the spline. */
+ x = ShaderUtils.clamp(x, 0, 1) * nspans;
+ span = (int) x;
+ if (span >= knot.length - 3) {
+ span = knot.length - 3;
+ }
+ x -= span;
+ /* Evaluate the span cubic at x using Horner’s rule. */
+ c3 = CR00 * knot[span + 0] + CR01 * knot[span + 1] + CR02 * knot[span + 2] + CR03 * knot[span + 3];
+ c2 = CR10 * knot[span + 0] + CR11 * knot[span + 1] + CR12 * knot[span + 2] + CR13 * knot[span + 3];
+ c1 = CR20 * knot[span + 0] + CR21 * knot[span + 1] + CR22 * knot[span + 2] + CR23 * knot[span + 3];
+ c0 = CR30 * knot[span + 0] + CR31 * knot[span + 1] + CR32 * knot[span + 2] + CR33 * knot[span + 3];
+ return ((c3 * x + c2) * x + c1) * x + c0;
+ }
+
+ public static final float[] spline(final float x, final float[][] knots) {
+ float[] retval = new float[knots.length];
+ for (int i = 0; i < knots.length; i++) {
+ retval[i] = ShaderUtils.spline(x, knots[i]);
+ }
+ return retval;
+ }
+
+ public static final float gammaCorrection(final float gamma, final float x) {
+ return (float) Math.pow(x, 1 / gamma);
+ }
+
+ public static final float bias(final float b, final float x) {
+ return (float) Math.pow(x, Math.log(b) / Math.log(0.5));
+ }
+
+ public static final float gain(final float g, final float x) {
+ return x < 0.5 ? ShaderUtils.bias(1 - g, 2 * x) / 2 : 1 - ShaderUtils.bias(1 - g, 2 - 2 * x) / 2;
+ }
+
+ public static final float sinValue(final float s, final float minFreq, final float maxFreq, final float swidth) {
+ float value = 0;
+ float cutoff = ShaderUtils.clamp(0.5f / swidth, 0, maxFreq);
+ float f;
+ for (f = minFreq; f < 0.5 * cutoff; f *= 2) {
+ value += Math.sin(2 * Math.PI * f * s) / f;
+ }
+ float fade = ShaderUtils.clamp(2 * (cutoff - f) / cutoff, 0, 1);
+ value += fade * Math.sin(2 * Math.PI * f * s) / f;
+ return value;
+ }
+
+ public static final float length(final float x, final float y, final float z) {
+ return (float) Math.sqrt(x * x + y * y + z * z);
+ }
+
+ public static final float[] rotate(final float[] v, final float[][] m) {
+ float x = v[0] * m[0][0] + v[1] * m[0][1] + v[2] * m[0][2];
+ float y = v[0] * m[1][0] + v[1] * m[1][1] + v[2] * m[1][2];
+ float z = v[0] * m[2][0] + v[1] * m[2][1] + v[2] * m[2][2];
+ return new float[] { x, y, z };
+ }
+
+ public static final float[][] calcRotationMatrix(final float ax, final float ay, final float az) {
+ float[][] retval = new float[3][3];
+ float cax = (float) Math.cos(ax);
+ float sax = (float) Math.sin(ax);
+ float cay = (float) Math.cos(ay);
+ float say = (float) Math.sin(ay);
+ float caz = (float) Math.cos(az);
+ float saz = (float) Math.sin(az);
+
+ retval[0][0] = cay * caz;
+ retval[0][1] = -cay * saz;
+ retval[0][2] = say;
+ retval[1][0] = sax * say * caz + cax * saz;
+ retval[1][1] = -sax * say * saz + cax * caz;
+ retval[1][2] = -sax * cay;
+ retval[2][0] = -cax * say * caz + sax * saz;
+ retval[2][1] = cax * say * saz + sax * caz;
+ retval[2][2] = cax * cay;
+
+ return retval;
+ }
+
+ public static final float[] normalize(final float[] v) {
+ float l = ShaderUtils.length(v);
+ float[] r = new float[v.length];
+ int i = 0;
+ for (float vv : v) {
+ r[i++] = vv / l;
+ }
+ return r;
+ }
+
+ public static final float length(final float[] v) {
+ float s = 0;
+ for (float vv : v) {
+ s += vv * vv;
+ }
+ return (float) Math.sqrt(s);
+ }
+
+ public static final ByteBuffer getImageDataFromImage(BufferedImage bufferedImage) {
+ WritableRaster wr;
+ DataBuffer db;
+
+ BufferedImage bi = new BufferedImage(128, 64, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = bi.createGraphics();
+ g.drawImage(bufferedImage, null, null);
+ bufferedImage = bi;
+ wr = bi.getRaster();
+ db = wr.getDataBuffer();
+
+ DataBufferInt dbi = (DataBufferInt) db;
+ int[] data = dbi.getData();
+
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(data.length * 4);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ byteBuffer.asIntBuffer().put(data);
+ byteBuffer.flip();
+
+ return byteBuffer;
+ }
+
+ public static float frac(float f) {
+ return f - ShaderUtils.floor(f);
+ }
+
+ public static float[] floor(float[] fs) {
+ float[] retval = new float[fs.length];
+ for (int i = 0; i < fs.length; i++) {
+ retval[i] = ShaderUtils.floor(fs[i]);
+ }
+ return retval;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/basis/FilteredBasis.java b/engine/src/terrain/com/jme3/terrain/noise/basis/FilteredBasis.java
new file mode 100644
index 0000000..53d0938
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/basis/FilteredBasis.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.basis;
+
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.terrain.noise.Basis;
+import com.jme3.terrain.noise.filter.AbstractFilter;
+import com.jme3.terrain.noise.modulator.Modulator;
+
+public class FilteredBasis extends AbstractFilter implements Basis {
+
+ private Basis basis;
+ private List<Modulator> modulators = new ArrayList<Modulator>();
+ private float scale;
+
+ public FilteredBasis() {}
+
+ public FilteredBasis(Basis basis) {
+ this.basis = basis;
+ }
+
+ public Basis getBasis() {
+ return this.basis;
+ }
+
+ public void setBasis(Basis basis) {
+ this.basis = basis;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer data, int size) {
+ return data;
+ }
+
+ @Override
+ public void init() {
+ this.basis.init();
+ }
+
+ @Override
+ public Basis setScale(float scale) {
+ this.scale = scale;
+ return this;
+ }
+
+ @Override
+ public float getScale() {
+ return this.scale;
+ }
+
+ @Override
+ public Basis addModulator(Modulator modulator) {
+ this.modulators.add(modulator);
+ return this;
+ }
+
+ @Override
+ public float value(float x, float y, float z) {
+ throw new UnsupportedOperationException(
+ "Method value cannot be called on FilteredBasis and its descendants. Use getBuffer instead!");
+ }
+
+ @Override
+ public FloatBuffer getBuffer(float sx, float sy, float base, int size) {
+ int margin = this.getMargin(size, 0);
+ int workSize = size + 2 * margin;
+ FloatBuffer retval = this.basis.getBuffer(sx - margin, sy - margin, base, workSize);
+ return this.clip(this.doFilter(sx, sy, base, retval, workSize), workSize, size, margin);
+ }
+
+ public FloatBuffer clip(FloatBuffer buf, int origSize, int newSize, int offset) {
+ FloatBuffer result = FloatBuffer.allocate(newSize * newSize);
+
+ float[] orig = buf.array();
+ for (int i = offset; i < offset + newSize; i++) {
+ result.put(orig, i * origSize + offset, newSize);
+ }
+
+ return result;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/basis/ImprovedNoise.java b/engine/src/terrain/com/jme3/terrain/noise/basis/ImprovedNoise.java
new file mode 100644
index 0000000..9233ef8
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/basis/ImprovedNoise.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.basis;
+
+import com.jme3.terrain.noise.ShaderUtils;
+
+// JAVA REFERENCE IMPLEMENTATION OF IMPROVED NOISE - COPYRIGHT 2002 KEN PERLIN.
+/**
+ * Perlin's default implementation of Improved Perlin Noise
+ * designed to work with Noise base
+ */
+public final class ImprovedNoise extends Noise {
+
+ @Override
+ public void init() {
+
+ }
+
+ static public float noise(float x, float y, float z) {
+ int X = ShaderUtils.floor(x), // FIND UNIT CUBE THAT
+ Y = ShaderUtils.floor(y), // CONTAINS POINT.
+ Z = ShaderUtils.floor(z);
+ x -= X; // FIND RELATIVE X,Y,Z
+ y -= Y; // OF POINT IN CUBE.
+ z -= Z;
+ X = X & 255;
+ Y = Y & 255;
+ Z = Z & 255;
+ float u = ImprovedNoise.fade(x), // COMPUTE FADE CURVES
+ v = ImprovedNoise.fade(y), // FOR EACH OF X,Y,Z.
+ w = ImprovedNoise.fade(z);
+ int A = ImprovedNoise.p[X] + Y;
+ int AA = ImprovedNoise.p[A] + Z;
+ int AB = ImprovedNoise.p[A + 1] + Z;
+ int B = ImprovedNoise.p[X + 1] + Y;
+ int BA = ImprovedNoise.p[B] + Z;
+ int BB = ImprovedNoise.p[B + 1] + Z;
+
+ return ImprovedNoise.lerp(
+ w,
+ ImprovedNoise.lerp(
+ v,
+ ImprovedNoise.lerp(u, ImprovedNoise.grad3(ImprovedNoise.p[AA], x, y, z),
+ ImprovedNoise.grad3(ImprovedNoise.p[BA], x - 1, y, z)), // BLENDED
+ ImprovedNoise.lerp(u, ImprovedNoise.grad3(ImprovedNoise.p[AB], x, y - 1, z), // RESULTS
+ ImprovedNoise.grad3(ImprovedNoise.p[BB], x - 1, y - 1, z))),// FROM
+ ImprovedNoise.lerp(v,
+ ImprovedNoise.lerp(u, ImprovedNoise.grad3(ImprovedNoise.p[AA + 1], x, y, z - 1), // CORNERS
+ ImprovedNoise.grad3(ImprovedNoise.p[BA + 1], x - 1, y, z - 1)), // OF
+ ImprovedNoise.lerp(u, ImprovedNoise.grad3(ImprovedNoise.p[AB + 1], x, y - 1, z - 1),
+ ImprovedNoise.grad3(ImprovedNoise.p[BB + 1], x - 1, y - 1, z - 1))));
+ }
+
+ static final float fade(final float t) {
+ return t * t * t * (t * (t * 6 - 15) + 10);
+ }
+
+ static final float lerp(final float t, final float a, final float b) {
+ return a + t * (b - a);
+ }
+
+ static float grad(final int hash, final float x, final float y, final float z) {
+ int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE
+ float u = h < 8 ? x : y, // INTO 12 GRADIENT DIRECTIONS.
+ v = h < 4 ? y : h == 12 || h == 14 ? x : z;
+ return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
+ }
+
+ static final float grad3(final int hash, final float x, final float y, final float z) {
+ int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE
+ return x * ImprovedNoise.GRAD3[h][0] + y * ImprovedNoise.GRAD3[h][1] + z * ImprovedNoise.GRAD3[h][2];
+ }
+
+ static final int p[] = new int[512], permutation[] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36,
+ 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11,
+ 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158,
+ 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80,
+ 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217,
+ 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183,
+ 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113,
+ 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235,
+ 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205,
+ 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 };
+
+ private static float[][] GRAD3 = new float[][] { { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 },
+ { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 }, { 1, 0, -1 }, { -1, 0, -1 }, { 0, -1, 1 },
+ { 0, 1, 1 } };
+
+ static {
+ for (int i = 0; i < 256; i++) {
+ ImprovedNoise.p[256 + i] = ImprovedNoise.p[i] = ImprovedNoise.permutation[i];
+ }
+ }
+
+ @Override
+ public float value(final float x, final float y, final float z) {
+ return ImprovedNoise.noise(this.scale * x, this.scale * y, this.scale * z);
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/basis/Noise.java b/engine/src/terrain/com/jme3/terrain/noise/basis/Noise.java
new file mode 100644
index 0000000..c9de6f5
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/basis/Noise.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.basis;
+
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.terrain.noise.Basis;
+import com.jme3.terrain.noise.modulator.Modulator;
+import com.jme3.terrain.noise.modulator.NoiseModulator;
+
+/**
+ * Utility base class for Noise implementations
+ *
+ * @author Anthyon
+ *
+ */
+public abstract class Noise implements Basis {
+
+ protected List<Modulator> modulators = new ArrayList<Modulator>();
+
+ protected float scale = 1.0f;
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName();
+ }
+
+ @Override
+ public FloatBuffer getBuffer(float sx, float sy, float base, int size) {
+ FloatBuffer retval = FloatBuffer.allocate(size * size);
+ for (int y = 0; y < size; y++) {
+ for (int x = 0; x < size; x++) {
+ retval.put(this.modulate((sx + x) / size, (sy + y) / size, base));
+ }
+ }
+ return retval;
+ }
+
+ public float modulate(float x, float y, float z) {
+ float retval = this.value(x, y, z);
+ for (Modulator m : this.modulators) {
+ if (m instanceof NoiseModulator) {
+ retval = m.value(retval);
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public Basis addModulator(Modulator modulator) {
+ this.modulators.add(modulator);
+ return this;
+ }
+
+ @Override
+ public Basis setScale(float scale) {
+ this.scale = scale;
+ return this;
+ }
+
+ @Override
+ public float getScale() {
+ return this.scale;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/basis/NoiseAggregator.java b/engine/src/terrain/com/jme3/terrain/noise/basis/NoiseAggregator.java
new file mode 100644
index 0000000..8a547a3
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/basis/NoiseAggregator.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.basis;
+
+import com.jme3.terrain.noise.Basis;
+
+/**
+ * A simple aggregator basis. Takes two basis functions and a rate and return
+ * some mixed values
+ *
+ * @author Anthyon
+ *
+ */
+public class NoiseAggregator extends Noise {
+
+ private final float rate;
+ private final Basis a;
+ private final Basis b;
+
+ public NoiseAggregator(final Basis a, final Basis b, final float rate) {
+ this.a = a;
+ this.b = b;
+ this.rate = rate;
+ }
+
+ @Override
+ public void init() {
+ this.a.init();
+ this.b.init();
+ }
+
+ @Override
+ public float value(final float x, final float y, final float z) {
+ return this.a.value(x, y, z) * (1 - this.rate) + this.rate * this.b.value(x, y, z);
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/AbstractFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/AbstractFilter.java
new file mode 100644
index 0000000..4174ba9
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/AbstractFilter.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.terrain.noise.Filter;
+
+public abstract class AbstractFilter implements Filter {
+
+ protected List<Filter> preFilters = new ArrayList<Filter>();
+ protected List<Filter> postFilters = new ArrayList<Filter>();
+
+ private boolean enabled = true;
+
+ @Override
+ public Filter addPreFilter(Filter filter) {
+ this.preFilters.add(filter);
+ return this;
+ }
+
+ @Override
+ public Filter addPostFilter(Filter filter) {
+ this.postFilters.add(filter);
+ return this;
+ }
+
+ @Override
+ public FloatBuffer doFilter(float sx, float sy, float base, FloatBuffer data, int size) {
+ if (!this.isEnabled()) {
+ return data;
+ }
+ FloatBuffer retval = data;
+ for (Filter f : this.preFilters) {
+ retval = f.doFilter(sx, sy, base, retval, size);
+ }
+ retval = this.filter(sx, sy, base, retval, size);
+ for (Filter f : this.postFilters) {
+ retval = f.doFilter(sx, sy, base, retval, size);
+ }
+ return retval;
+ }
+
+ public abstract FloatBuffer filter(float sx, float sy, float base, FloatBuffer buffer, int size);
+
+ @Override
+ public int getMargin(int size, int margin) {
+ // TODO sums up all the margins from filters... maybe there's a more
+ // efficient algorithm
+ if (!this.isEnabled()) {
+ return margin;
+ }
+ for (Filter f : this.preFilters) {
+ margin = f.getMargin(size, margin);
+ }
+ for (Filter f : this.postFilters) {
+ margin = f.getMargin(size, margin);
+ }
+ return margin;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/HydraulicErodeFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/HydraulicErodeFilter.java
new file mode 100644
index 0000000..13e2c5e
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/HydraulicErodeFilter.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+
+import com.jme3.terrain.noise.Basis;
+
+public class HydraulicErodeFilter extends AbstractFilter {
+
+ private Basis waterMap;
+ private Basis sedimentMap;
+ private float Kr;
+ private float Ks;
+ private float Ke;
+ private float Kc;
+ private float T;
+
+ public void setKc(float kc) {
+ this.Kc = kc;
+ }
+
+ public void setKe(float ke) {
+ this.Ke = ke;
+ }
+
+ public void setKr(float kr) {
+ this.Kr = kr;
+ }
+
+ public void setKs(float ks) {
+ this.Ks = ks;
+ }
+
+ public void setSedimentMap(Basis sedimentMap) {
+ this.sedimentMap = sedimentMap;
+ }
+
+ public void setT(float t) {
+ this.T = t;
+ }
+
+ public void setWaterMap(Basis waterMap) {
+ this.waterMap = waterMap;
+ }
+
+ @Override
+ public int getMargin(int size, int margin) {
+ return super.getMargin(size, margin) + 1;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer buffer, int workSize) {
+ float[] ga = buffer.array();
+ // float[] wa = this.waterMap.getBuffer(sx, sy, base, workSize).array();
+ // float[] sa = this.sedimentMap.getBuffer(sx, sy, base,
+ // workSize).array();
+ float[] wt = new float[workSize * workSize];
+ float[] st = new float[workSize * workSize];
+
+ int[] idxrel = { -workSize - 1, -workSize + 1, workSize - 1, workSize + 1 };
+
+ // step 1. water arrives and step 2. captures material
+ for (int y = 0; y < workSize; y++) {
+ for (int x = 0; x < workSize; x++) {
+ int idx = y * workSize + x;
+ float wtemp = this.Kr; // * wa[idx];
+ float stemp = this.Ks; // * sa[idx];
+ if (wtemp > 0) {
+ wt[idx] += wtemp;
+ if (stemp > 0) {
+ ga[idx] -= stemp * wt[idx];
+ st[idx] += stemp * wt[idx];
+ }
+ }
+
+ // step 3. water is transported to it's neighbours
+ float a = ga[idx] + wt[idx];
+ // float[] aj = new float[idxrel.length];
+ float amax = 0;
+ int amaxidx = -1;
+ float ac = 0;
+ float dtotal = 0;
+
+ for (int j = 0; j < idxrel.length; j++) {
+ if (idx + idxrel[j] > 0 && idx + idxrel[j] < workSize) {
+ float at = ga[idx + idxrel[j]] + wt[idx + idxrel[j]];
+ if (a - at > a - amax) {
+ dtotal += at;
+ amax = at;
+ amaxidx = j;
+ ac++;
+ }
+ }
+ }
+
+ float aa = (dtotal + a) / (ac + 1);
+ // for (int j = 0; j < idxrel.length; j++) {
+ // if (idx + idxrel[j] > 0 && idx + idxrel[j] < workSize && a -
+ // aj[j] > 0) {
+ if (amaxidx > -1) {
+ float dwj = Math.min(wt[idx], a - aa) * (a - amax) / dtotal;
+ float dsj = st[idx] * dwj / wt[idx];
+ wt[idx] -= dwj;
+ st[idx] -= dsj;
+ wt[idx + idxrel[amaxidx]] += dwj;
+ st[idx + idxrel[amaxidx]] += dsj;
+ }
+ // }
+
+ // step 4. water evaporates and deposits material
+ wt[idx] = wt[idx] * (1 - this.Ke);
+ if (wt[idx] < this.T) {
+ wt[idx] = 0;
+ }
+ float smax = this.Kc * wt[idx];
+ if (st[idx] > smax) {
+ ga[idx] += st[idx] - smax;
+ st[idx] -= st[idx] - smax;
+ }
+ }
+ }
+
+ return buffer;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/IterativeFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/IterativeFilter.java
new file mode 100644
index 0000000..3764256
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/IterativeFilter.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.terrain.noise.Filter;
+
+public class IterativeFilter extends AbstractFilter {
+
+ private int iterations;
+
+ private List<Filter> preIterateFilters = new ArrayList<Filter>();
+ private List<Filter> postIterateFilters = new ArrayList<Filter>();
+ private Filter filter;
+
+ @Override
+ public int getMargin(int size, int margin) {
+ if (!this.isEnabled()) {
+ return margin;
+ }
+ for (Filter f : this.preIterateFilters) {
+ margin = f.getMargin(size, margin);
+ }
+ margin = this.filter.getMargin(size, margin);
+ for (Filter f : this.postIterateFilters) {
+ margin = f.getMargin(size, margin);
+ }
+ return this.iterations * margin + super.getMargin(size, margin);
+ }
+
+ public void setIterations(int iterations) {
+ this.iterations = iterations;
+ }
+
+ public int getIterations() {
+ return this.iterations;
+ }
+
+ public IterativeFilter addPostIterateFilter(Filter filter) {
+ this.postIterateFilters.add(filter);
+ return this;
+ }
+
+ public IterativeFilter addPreIterateFilter(Filter filter) {
+ this.preIterateFilters.add(filter);
+ return this;
+ }
+
+ public void setFilter(Filter filter) {
+ this.filter = filter;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer data, int size) {
+ if (!this.isEnabled()) {
+ return data;
+ }
+ FloatBuffer retval = data;
+
+ for (int i = 0; i < this.iterations; i++) {
+ for (Filter f : this.preIterateFilters) {
+ retval = f.doFilter(sx, sy, base, retval, size);
+ }
+ retval = this.filter.doFilter(sx, sy, base, retval, size);
+ for (Filter f : this.postIterateFilters) {
+ retval = f.doFilter(sx, sy, base, retval, size);
+ }
+ }
+
+ return retval;
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/OptimizedErode.java b/engine/src/terrain/com/jme3/terrain/noise/filter/OptimizedErode.java
new file mode 100644
index 0000000..fc20da6
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/OptimizedErode.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+
+public class OptimizedErode extends AbstractFilter {
+
+ private float talus;
+ private int radius;
+
+ public OptimizedErode setRadius(int radius) {
+ this.radius = radius;
+ return this;
+ }
+
+ public int getRadius() {
+ return this.radius;
+ }
+
+ public OptimizedErode setTalus(float talus) {
+ this.talus = talus;
+ return this;
+ }
+
+ public float getTalus() {
+ return this.talus;
+ }
+
+ @Override
+ public int getMargin(int size, int margin) {
+ return super.getMargin(size, margin) + this.radius;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer buffer, int size) {
+ float[] tmp = buffer.array();
+ float[] retval = new float[tmp.length];
+
+ for (int y = this.radius + 1; y < size - this.radius; y++) {
+ for (int x = this.radius + 1; x < size - this.radius; x++) {
+ int idx = y * size + x;
+ float h = tmp[idx];
+
+ float horizAvg = 0;
+ int horizCount = 0;
+ float vertAvg = 0;
+ int vertCount = 0;
+
+ boolean horizT = false;
+ boolean vertT = false;
+
+ for (int i = 0; i >= -this.radius; i--) {
+ int idxV = (y + i) * size + x;
+ int idxVL = (y + i - 1) * size + x;
+ int idxH = y * size + x + i;
+ int idxHL = y * size + x + i - 1;
+ float hV = tmp[idxV];
+ float hH = tmp[idxH];
+
+ if (Math.abs(h - hV) > this.talus && Math.abs(h - tmp[idxVL]) > this.talus || vertT) {
+ vertT = true;
+ } else {
+ if (Math.abs(h - hV) <= this.talus) {
+ vertAvg += hV;
+ vertCount++;
+ }
+ }
+
+ if (Math.abs(h - hH) > this.talus && Math.abs(h - tmp[idxHL]) > this.talus || horizT) {
+ horizT = true;
+ } else {
+ if (Math.abs(h - hH) <= this.talus) {
+ horizAvg += hH;
+ horizCount++;
+ }
+ }
+ }
+
+ retval[idx] = 0.5f * (vertAvg / (vertCount > 0 ? vertCount : 1) + horizAvg / (horizCount > 0 ? horizCount : 1));
+ }
+ }
+ return FloatBuffer.wrap(retval);
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/PerturbFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/PerturbFilter.java
new file mode 100644
index 0000000..f510f90
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/PerturbFilter.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+import java.util.logging.Logger;
+
+import com.jme3.terrain.noise.ShaderUtils;
+import com.jme3.terrain.noise.fractal.FractalSum;
+
+public class PerturbFilter extends AbstractFilter {
+
+ private float magnitude;
+
+ @Override
+ public int getMargin(int size, int margin) {
+ margin = super.getMargin(size, margin);
+ return (int) Math.floor(this.magnitude * (margin + size) + margin);
+ }
+
+ public void setMagnitude(float magnitude) {
+ this.magnitude = magnitude;
+ }
+
+ public float getMagnitude() {
+ return this.magnitude;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer data, int workSize) {
+ float[] arr = data.array();
+ int origSize = (int) Math.ceil(workSize / (2 * this.magnitude + 1));
+ int offset = (workSize - origSize) / 2;
+ Logger.getLogger(PerturbFilter.class.getCanonicalName()).info(
+ "Found origSize : " + origSize + " and offset: " + offset + " for workSize : " + workSize + " and magnitude : "
+ + this.magnitude);
+ float[] retval = new float[workSize * workSize];
+ float[] perturbx = new FractalSum().setOctaves(8).setScale(5f).getBuffer(sx, sy, base, workSize).array();
+ float[] perturby = new FractalSum().setOctaves(8).setScale(5f).getBuffer(sx, sy, base + 1, workSize).array();
+ for (int y = 0; y < workSize; y++) {
+ for (int x = 0; x < workSize; x++) {
+ // Perturb our coordinates
+ float noisex = perturbx[y * workSize + x];
+ float noisey = perturby[y * workSize + x];
+
+ int px = (int) (origSize * noisex * this.magnitude);
+ int py = (int) (origSize * noisey * this.magnitude);
+
+ float c00 = arr[this.wrap(y - py, workSize) * workSize + this.wrap(x - px, workSize)];
+ float c01 = arr[this.wrap(y - py, workSize) * workSize + this.wrap(x + px, workSize)];
+ float c10 = arr[this.wrap(y + py, workSize) * workSize + this.wrap(x - px, workSize)];
+ float c11 = arr[this.wrap(y + py, workSize) * workSize + this.wrap(x + px, workSize)];
+
+ float c0 = ShaderUtils.mix(c00, c01, noisex);
+ float c1 = ShaderUtils.mix(c10, c11, noisex);
+ retval[y * workSize + x] = ShaderUtils.mix(c0, c1, noisey);
+ }
+ }
+ return FloatBuffer.wrap(retval);
+ }
+
+ private int wrap(int v, int size) {
+ if (v < 0) {
+ return v + size - 1;
+ } else if (v >= size) {
+ return v - size;
+ } else {
+ return v;
+ }
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/SmoothFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/SmoothFilter.java
new file mode 100644
index 0000000..1c20c24
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/SmoothFilter.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+
+public class SmoothFilter extends AbstractFilter {
+
+ private int radius;
+ private float effect;
+
+ public void setRadius(int radius) {
+ this.radius = radius;
+ }
+
+ public int getRadius() {
+ return this.radius;
+ }
+
+ public void setEffect(float effect) {
+ this.effect = effect;
+ }
+
+ public float getEffect() {
+ return this.effect;
+ }
+
+ @Override
+ public int getMargin(int size, int margin) {
+ return super.getMargin(size, margin) + this.radius;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer buffer, int size) {
+ float[] data = buffer.array();
+ float[] retval = new float[data.length];
+
+ for (int y = this.radius; y < size - this.radius; y++) {
+ for (int x = this.radius; x < size - this.radius; x++) {
+ int idx = y * size + x;
+ float n = 0;
+ for (int i = -this.radius; i < this.radius + 1; i++) {
+ for (int j = -this.radius; j < this.radius + 1; j++) {
+ n += data[(y + i) * size + x + j];
+ }
+ }
+ retval[idx] = this.effect * n / (4 * this.radius * (this.radius + 1) + 1) + (1 - this.effect) * data[idx];
+ }
+ }
+
+ return FloatBuffer.wrap(retval);
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/filter/ThermalErodeFilter.java b/engine/src/terrain/com/jme3/terrain/noise/filter/ThermalErodeFilter.java
new file mode 100644
index 0000000..2e49679
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/filter/ThermalErodeFilter.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.filter;
+
+import java.nio.FloatBuffer;
+
+public class ThermalErodeFilter extends AbstractFilter {
+
+ private float talus;
+ private float c;
+
+ public ThermalErodeFilter setC(float c) {
+ this.c = c;
+ return this;
+ }
+
+ public ThermalErodeFilter setTalus(float talus) {
+ this.talus = talus;
+ return this;
+ }
+
+ @Override
+ public int getMargin(int size, int margin) {
+ return super.getMargin(size, margin) + 1;
+ }
+
+ @Override
+ public FloatBuffer filter(float sx, float sy, float base, FloatBuffer buffer, int workSize) {
+ float[] ga = buffer.array();
+ float[] sa = new float[workSize * workSize];
+
+ int[] idxrel = { -workSize - 1, -workSize + 1, workSize - 1, workSize + 1 };
+
+ for (int y = 0; y < workSize; y++) {
+ for (int x = 0; x < workSize; x++) {
+ int idx = y * workSize + x;
+ ga[idx] += sa[idx];
+ sa[idx] = 0;
+
+ float[] deltas = new float[idxrel.length];
+ float deltaMax = this.talus;
+ float deltaTotal = 0;
+
+ for (int j = 0; j < idxrel.length; j++) {
+ if (idx + idxrel[j] > 0 && idx + idxrel[j] < ga.length) {
+ float dj = ga[idx] - ga[idx + idxrel[j]];
+ if (dj > this.talus) {
+ deltas[j] = dj;
+ deltaTotal += dj;
+ if (dj > deltaMax) {
+ deltaMax = dj;
+ }
+ }
+ }
+ }
+
+ for (int j = 0; j < idxrel.length; j++) {
+ if (deltas[j] != 0) {
+ float d = this.c * (deltaMax - this.talus) * deltas[j] / deltaTotal;
+ if (d > ga[idx] + sa[idx]) {
+ d = ga[idx] + sa[idx];
+ }
+ sa[idx] -= d;
+ sa[idx + idxrel[j]] += d;
+ }
+ deltas[j] = 0;
+ }
+ }
+ }
+
+ return buffer;
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/fractal/Fractal.java b/engine/src/terrain/com/jme3/terrain/noise/fractal/Fractal.java
new file mode 100644
index 0000000..9b53447
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/fractal/Fractal.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.fractal;
+
+import com.jme3.terrain.noise.Basis;
+
+/**
+ * Interface for a general fractal basis.
+ *
+ * Takes any number of basis funcions to work with and a few common parameters
+ * for noise fractals
+ *
+ * @author Anthyon
+ *
+ */
+public interface Fractal extends Basis {
+
+ public Fractal setOctaves(final float octaves);
+
+ public Fractal setFrequency(final float frequency);
+
+ public Fractal setRoughness(final float roughness);
+
+ public Fractal setAmplitude(final float amplitude);
+
+ public Fractal setLacunarity(final float lacunarity);
+
+ public Fractal addBasis(Basis basis);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/fractal/FractalSum.java b/engine/src/terrain/com/jme3/terrain/noise/fractal/FractalSum.java
new file mode 100644
index 0000000..5fe5dcb
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/fractal/FractalSum.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.fractal;
+
+import com.jme3.terrain.noise.Basis;
+import com.jme3.terrain.noise.ShaderUtils;
+import com.jme3.terrain.noise.basis.ImprovedNoise;
+import com.jme3.terrain.noise.basis.Noise;
+
+/**
+ * FractalSum is the simplest form of fractal functions summing up a few octaves
+ * of the noise value with an ever decreasing (0 < roughness < 1) amplitude
+ *
+ * lacunarity = 2.0f is the classical octave distance
+ *
+ * Note: though noise basis functions are generally designed to return value
+ * between -1..1, there sum can easily be made to extend out of this range. To
+ * handle this is up to the user.
+ *
+ * @author Anthyon
+ *
+ */
+public class FractalSum extends Noise implements Fractal {
+
+ private Basis basis;
+ private float lacunarity;
+ private float amplitude;
+ private float roughness;
+ private float frequency;
+ private float octaves;
+ private int maxFreq;
+
+ public FractalSum() {
+ this.basis = new ImprovedNoise();
+ this.lacunarity = 2.124367f;
+ this.amplitude = 1.0f;
+ this.roughness = 0.6f;
+ this.frequency = 1f;
+ this.setOctaves(1);
+ }
+
+ @Override
+ public float value(final float x, final float y, final float z) {
+ float total = 0;
+
+ for (float f = this.frequency, a = this.amplitude; f < this.maxFreq; f *= this.lacunarity, a *= this.roughness) {
+ total += this.basis.value(this.scale * x * f, this.scale * y * f, this.scale * z * f) * a;
+ }
+
+ return ShaderUtils.clamp(total, -1, 1);
+ }
+
+ @Override
+ public Fractal addBasis(final Basis basis) {
+ this.basis = basis;
+ return this;
+ }
+
+ public float getOctaves() {
+ return this.octaves;
+ }
+
+ @Override
+ public Fractal setOctaves(final float octaves) {
+ this.octaves = octaves;
+ this.maxFreq = 1 << (int) octaves;
+ return this;
+ }
+
+ public float getFrequency() {
+ return this.frequency;
+ }
+
+ @Override
+ public Fractal setFrequency(final float frequency) {
+ this.frequency = frequency;
+ return this;
+ }
+
+ public float getRoughness() {
+ return this.roughness;
+ }
+
+ @Override
+ public Fractal setRoughness(final float roughness) {
+ this.roughness = roughness;
+ return this;
+ }
+
+ public float getAmplitude() {
+ return this.amplitude;
+ }
+
+ @Override
+ public Fractal setAmplitude(final float amplitude) {
+ this.amplitude = amplitude;
+ return this;
+ }
+
+ public float getLacunarity() {
+ return this.lacunarity;
+ }
+
+ @Override
+ public Fractal setLacunarity(final float lacunarity) {
+ this.lacunarity = lacunarity;
+ return this;
+ }
+
+ @Override
+ public void init() {
+
+ }
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/modulator/CatRom2.java b/engine/src/terrain/com/jme3/terrain/noise/modulator/CatRom2.java
new file mode 100644
index 0000000..3f09c29
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/modulator/CatRom2.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.modulator;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.jme3.terrain.noise.ShaderUtils;
+
+public class CatRom2 implements Modulator {
+
+ private int sampleRate = 100;
+
+ private final float[] table;
+
+ private static Map<Integer, CatRom2> instances = new HashMap<Integer, CatRom2>();
+
+ public CatRom2(final int sampleRate) {
+ this.sampleRate = sampleRate;
+ this.table = new float[4 * sampleRate + 1];
+ for (int i = 0; i < 4 * sampleRate + 1; i++) {
+ float x = i / (float) sampleRate;
+ x = (float) Math.sqrt(x);
+ if (x < 1) {
+ this.table[i] = 0.5f * (2 + x * x * (-5 + x * 3));
+ } else {
+ this.table[i] = 0.5f * (4 + x * (-8 + x * (5 - x)));
+ }
+ }
+ }
+
+ public static CatRom2 getInstance(final int sampleRate) {
+ if (!CatRom2.instances.containsKey(sampleRate)) {
+ CatRom2.instances.put(sampleRate, new CatRom2(sampleRate));
+ }
+ return CatRom2.instances.get(sampleRate);
+ }
+
+ @Override
+ public float value(final float... in) {
+ if (in[0] >= 4) {
+ return 0;
+ }
+ in[0] = in[0] * this.sampleRate + 0.5f;
+ int i = ShaderUtils.floor(in[0]);
+ if (i >= 4 * this.sampleRate + 1) {
+ return 0;
+ }
+ return this.table[i];
+ }
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/modulator/Modulator.java b/engine/src/terrain/com/jme3/terrain/noise/modulator/Modulator.java
new file mode 100644
index 0000000..28bfd1c
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/modulator/Modulator.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.modulator;
+
+public interface Modulator {
+
+ public float value(float... in);
+
+}
diff --git a/engine/src/terrain/com/jme3/terrain/noise/modulator/NoiseModulator.java b/engine/src/terrain/com/jme3/terrain/noise/modulator/NoiseModulator.java
new file mode 100644
index 0000000..38a3bd6
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/noise/modulator/NoiseModulator.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011, Novyon Events
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @author Anthyon
+ */
+package com.jme3.terrain.noise.modulator;
+
+public interface NoiseModulator extends Modulator {
+
+}