jme3utilities.mesh.Icosphere Maven / Gradle / Ivy
Show all versions of Heart Show documentation
/*
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 the copyright holder 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 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.
*/
package jme3utilities.mesh;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import jme3utilities.MyMesh;
import jme3utilities.Validate;
import jme3utilities.math.MyMath;
import jme3utilities.math.MyVector3f;
/**
* A 3-D, static, Triangles-mode mesh (with indices, normals, and texture
* coordinates) that approximates a sphere, generated by subdividing the faces
* of a regular icosahedron. The resulting mesh is more isotropic than a U-V
* sphere.
*
* The texture coordinates are similar to Sphere.TextureMode.Projected, only
* with the U values mirrored and offset. Also, there is a boundary issue with
* the texture coordinates; this issue is very noticeable when the number of
* refinement steps is <3.
*
* @author jayfella
*/
public class Icosphere extends Mesh {
// *************************************************************************
// constants and loggers
/**
* golden ratio = 1.618...
*/
final private static float phi = MyMath.phi;
/**
* number of axes in a vector
*/
final private static int numAxes = 3;
/**
* number of vertices per triangle
*/
final private static int vpt = 3;
/**
* vertex indices of the 20 triangular faces in a regular icosahedron
*/
final private static int[] icoIndices = {
0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1
};
/**
* message logger for this class
*/
final public static Logger logger
= Logger.getLogger(Icosphere.class.getName());
/**
* vertex locations in a regular icosahedron with radius=1.9021...
*/
final private static Vector3f[] icoLocations = {
new Vector3f(-1f, phi, 0f), new Vector3f(1f, phi, 0f),
new Vector3f(-1f, -phi, 0f), new Vector3f(1f, -phi, 0f),
new Vector3f(0f, -1f, phi), new Vector3f(0f, 1f, phi),
new Vector3f(0f, -1f, -phi), new Vector3f(0f, 1f, -phi),
new Vector3f(phi, 0f, -1f), new Vector3f(phi, 0f, 1f),
new Vector3f(-phi, 0f, -1f), new Vector3f(-phi, 0f, 1f)
};
// *************************************************************************
// fields
/**
* distance of each vertex from the center (>0)
*/
final private float radius;
/**
* next vertex index to be assigned
*/
private int nextVertexIndex = 0;
/**
* map vertex indices to location vectors in mesh coordinates, all with
* length=radius
*/
final private List locations = new ArrayList<>(162);
/**
* cache to avoid duplicate vertices: map index pairs to midpoint indices
*/
final private Map midpointCache = new HashMap<>(480);
// *************************************************************************
// constructors
/**
* No-argument constructor needed by SavableClassUtil.
*/
protected Icosphere() {
this.radius = 1f;
}
/**
* Instantiate an icosphere with the specified radius and number of
* refinement steps:
* -
* 0 steps → 12 vertices, 30 edges, 20 triangular faces
*
-
* 1 step → 42 vertices, 120 edges, 80 triangular faces
*
-
* 2 steps → 162 vertices, 480 edges, 320 triangular faces
*
-
* etcetera
*
*
* The center is at (0,0,0). All triangles face outward.
*
* @param numRefineSteps the desired number of refinement steps (≥0)
* @param radius the desired radius (in mesh units, >0)
*/
public Icosphere(int numRefineSteps, float radius) {
Validate.nonNegative(numRefineSteps, "number of refinement steps");
Validate.positive(radius, "radius");
this.radius = radius;
// Add the 12 vertices of a regular icosahedron of radius=1.
for (Vector3f icoLocation : icoLocations) {
addVertex(icoLocation);
}
// Add the 20 triangular faces of a regular icosahedron.
List faces = new ArrayList<>(60);
for (int icoIndex : icoIndices) {
faces.add(icoIndex);
}
for (int stepIndex = 0; stepIndex < numRefineSteps; ++stepIndex) {
List newFaces = new ArrayList<>(4 * faces.size());
/*
* A refinement step: cut each edge in half; for each
* triangle in faces, add 4 triangles to newFaces.
*/
for (int j = 0; j < faces.size(); j += vpt) {
int v1 = faces.get(j);
int v2 = faces.get(j + 1);
int v3 = faces.get(j + 2);
int a = midpointIndex(v1, v2);
int b = midpointIndex(v2, v3);
int c = midpointIndex(v3, v1);
newFaces.add(v1);
newFaces.add(a);
newFaces.add(c);
newFaces.add(v2);
newFaces.add(b);
newFaces.add(a);
newFaces.add(v3);
newFaces.add(c);
newFaces.add(b);
newFaces.add(a);
newFaces.add(b);
newFaces.add(c);
}
faces = newFaces;
}
midpointCache.clear();
int numVertices = locations.size();
int numFloats = numAxes * numVertices;
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(numFloats);
for (Vector3f pos : locations) {
posBuffer.put(pos.x).put(pos.y).put(pos.z);
}
posBuffer.flip();
setBuffer(VertexBuffer.Type.Position, numAxes, posBuffer);
int numIndices = faces.size();
IndexBuffer ib = IndexBuffer.createIndexBuffer(numVertices, numIndices);
for (int vertexIndex : faces) {
ib.put(vertexIndex);
}
VertexBuffer.Format ibFormat = ib.getFormat();
Buffer ibData = ib.getBuffer();
ibData.flip();
setBuffer(VertexBuffer.Type.Index, vpt, ibFormat, ibData);
FloatBuffer uvBuffer = BufferUtils.createFloatBuffer(2 * numVertices);
for (Vector3f pos : locations) {
Vector2f longLat = cartesianToSpherical(pos);
float u = 0.5f + longLat.x / FastMath.TWO_PI;
// u = MyMath.modulo(0.5f - u, 1f);// to match TextureMode.Projected
float v = 0.5f + longLat.y / FastMath.PI;
uvBuffer.put(u).put(v);
}
uvBuffer.flip();
setBuffer(VertexBuffer.Type.TexCoord, 2, uvBuffer);
MyMesh.addSphereNormals(this);
locations.clear();
updateBound();
setStatic();
}
// *************************************************************************
// private methods
/**
* Add a vertex to the lists of locations and normals.
*
* @param location the approximate vertex location (in mesh coordinates, not
* null, unaffected)
* @return the index assigned to the new vertex (≥0)
*/
private int addVertex(Vector3f location) {
float length = location.length();
locations.add(location.mult(radius / length));
int result = nextVertexIndex;
++nextVertexIndex;
return result;
}
/**
* Transform 3-D Cartesian coordinates to longitude and latitude.
*
* @param input the location to transform (z=distance north of the
* equatorial plane, not null, unaffected)
* @return a new vector (x=west longitude in radians, y=north latitude in
* radians)
*/
private static Vector2f cartesianToSpherical(Vector3f input) {
Vector2f result = new Vector2f();
float length = input.length();
if (input.x != 0f || input.y != 0f) {
result.x = -FastMath.atan2(input.y, input.x);
} else {
result.x = 0f;
}
if (length > 0f) {
result.y = FastMath.asin(input.z / length);
} else {
result.y = 0f;
}
return result;
}
/**
* Determine the index of the vertex halfway between the indexed vertices.
*
* @param p1 the index of the first input vertex (≥0)
* @param p2 the index of the 2nd input vertex (≥0)
* @return the midpoint index (≥0)
*/
private int midpointIndex(int p1, int p2) {
// Check whether the midpoint has already been assigned an index.
boolean firstIsSmaller = p1 < p2;
long smallerIndex = firstIsSmaller ? p1 : p2;
long greaterIndex = firstIsSmaller ? p2 : p1;
long key = (smallerIndex << 32) + greaterIndex;
Integer cachedIndex = midpointCache.get(key);
if (cachedIndex != null) {
return cachedIndex;
}
// The midpoint vertex is not in the cache: calculate its location.
Vector3f loc1 = locations.get(p1);
Vector3f loc2 = locations.get(p2);
Vector3f middleLocation = MyVector3f.midpoint(loc1, loc2, null);
// addVertex() adjusts the location to the sphere.
int newIndex = addVertex(middleLocation);
// Add the new vertex to the midpoint cache.
midpointCache.put(key, newIndex);
return newIndex;
}
}