jme3utilities.mesh.Octasphere Maven / Gradle / Ivy
Show all versions of Heart Show documentation
/*
Copyright (c) 2020-2023, Stephen Gold
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 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.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.MyVector3f;
/**
* A static, Triangles-mode mesh (with indices, normals, and texture
* coordinates) that approximates a sphere, generated by subdividing the faces
* of a regular octahedron. The resulting mesh is more isotropic than a U-V
* sphere and handles textures better than an icosphere.
*
* The center is at (0,0,0). All triangles face outward with right-handed
* winding.
*
* The texture coordinates are generated for an equirectangular projection
* similar to Sphere.TextureMode.Projected, only with the U values mirrored and
* offset:
*
* - U is the azimuthal angle in revs. It ranges from 0 to 1.
*
- V is the polar angle, measured (in half revs) from the -Z axis. It ranges
* from 0 to 1.
*
*
* Vertices with Y=0 and X<1 lie on the seam. Those vertices are doubled and
* can have either U=0 or U=1.
*
* Vertices with X=0 and Y=0 lie at the poles. Those vertices are trebled and
* can have U=0 or 0.5 or 1.
*
* Derived from Icosphere by jayfella.
*
* @author Stephen Gold [email protected]
*/
public class Octasphere extends Mesh {
// *************************************************************************
// constants and loggers
/**
* number of axes in a vector
*/
final private static int numAxes = 3;
/**
* vertex indices of the 8 faces in a regular octahedron (outward-facing
* triangles with right-handed winding)
*
* Vertices [0] and [6] occupy (-1, 0, 0) in mesh space. In order to create
* a seam, vertex [0] will have U=0 and vertex [6] will have U=1.
*
* Vertices [4, 7, 9] occupy (0, 0, -1) in mesh space. Vertex [4] will have
* U=0.5, vertex [7] will have U=1, and vertex [9] will have U=0.
*
* Vertices [5, 8, 10] occupy (0, 0, 1) in mesh space. Vertex [5] will have
* U=0.5, vertex [8] will have U=1, and vertex [10] will have U=0.
*/
final private static int[] octaIndices = {
6, 2, 8, // -X -Y +Z face
1, 4, 3, // +X +Y -Z face
0, 3, 9, // -X +Y -Z face
1, 5, 2, // +X -Y +Z face
6, 7, 2, // -X -Y -Z face
1, 3, 5, // +X +Y +Z face
0, 10, 3, // -X +Y +Z face
1, 2, 4 // +X -Y -Z face
};
/**
* number of vertices per triangle
*/
final private static int vpt = 3;
/**
* message logger for this class
*/
final public static Logger logger
= Logger.getLogger(Octasphere.class.getName());
/**
* vertex locations in a regular octahedron with radius=1
*/
final private static Vector3f[] octaLocations = {
new Vector3f(-1f, 0f, 0f), // [0]
new Vector3f(+1f, 0f, 0f), // [1]
new Vector3f(0f, -1f, 0f), // [2]
new Vector3f(0f, +1f, 0f), // [3]
new Vector3f(0f, 0f, -1f), // [4]
new Vector3f(0f, 0f, +1f) // [5]
};
// *************************************************************************
// 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 U coordinates for vertices with Y=0
*/
final private List uOverrides = new ArrayList<>(305);
/**
* map vertex indices to location vectors in mesh coordinates, all with
* length=radius
*/
final private List locations = new ArrayList<>(305);
/**
* cache to avoid duplicate vertices: map index pairs to midpoint indices
*/
final private Map midpointCache = new HashMap<>(294);
// *************************************************************************
// constructors
/**
* No-argument constructor needed by SavableClassUtil.
*/
protected Octasphere() {
this.radius = 1f;
}
/**
* Instantiate an Octasphere with the specified radius and number of
* refinement steps:
* -
* 0 steps → 11 unique vertices and 8 triangular faces
*
-
* 1 step → 27 unique vertices and 32 triangular faces
*
-
* 2 steps → 83 unique vertices and 128 triangular faces
*
-
* 3 steps → 291 unique vertices and 512 triangular faces
*
-
* 4 steps → 1091 unique vertices and 2048 triangular faces
*
-
* etcetera
*
*
* @param numRefineSteps number of refinement steps (≥0, ≤13)
* @param radius radius (in mesh units, >0)
*/
public Octasphere(int numRefineSteps, float radius) {
Validate.inRange(numRefineSteps, "number of refinement steps", 0, 13);
Validate.positive(radius, "radius");
this.radius = radius;
// Add the 6 vertices of a regular octahedron with radius=1.
addVertex(octaLocations[0], 0f); // [0]
addVertex(octaLocations[1], 0.5f); // [1]
addVertex(octaLocations[2], null); // [2]
addVertex(octaLocations[3], null); // [3]
addVertex(octaLocations[4], 0.5f); // [4]
addVertex(octaLocations[5], 0.5f); // [5]
// Add duplicate vertices with U=1.
addVertex(octaLocations[0], 1f); // [6]
addVertex(octaLocations[4], 1f); // [7]
addVertex(octaLocations[5], 1f); // [8]
// Add triplicate polar vertices with U=0.
addVertex(octaLocations[4], 0f); // [9]
addVertex(octaLocations[5], 0f); // [10]
// Add the 8 triangular faces of a regular octahedron.
List faces = new ArrayList<>(24);
for (int octaIndex : octaIndices) {
faces.add(octaIndex);
}
for (int stepIndex = 0; stepIndex < numRefineSteps; ++stepIndex) {
List newFaces = new ArrayList<>(4 * faces.size());
/*
* a refinement step: divide each edge into 2 halves;
* for each triangle in {@code faces},
* add 4 triangles to {@code 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;
}
// System.out.println("numRefineSteps = " + numRefineSteps);
// System.out.println("numVertices = " + locations.size());
// System.out.println("numFaces = " + faces.size() / vpt);
// System.out.println("numCacheEntries = " + midpointCache.size());
// System.out.println();
//
assert locations.size() == uOverrides.size();
midpointCache.clear();
assert faces.size() == 3 << (3 + 2 * numRefineSteps);
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 (int i = 0; i < numVertices; ++i) {
Vector3f pos = locations.get(i); // alias
float longitude = longitude(pos);
float u;
if (pos.y == 0f) {
u = uOverrides.get(i);
} else {
assert uOverrides.get(i) == null;
u = 0.5f + longitude / FastMath.TWO_PI;
}
float latitude = latitude(pos);
float v = 0.5f + latitude / FastMath.PI;
uvBuffer.put(u).put(v);
}
uvBuffer.flip();
setBuffer(VertexBuffer.Type.TexCoord, 2, uvBuffer);
locations.clear();
uOverrides.clear();
MyMesh.addSphereNormals(this);
updateBound();
setStatic();
}
// *************************************************************************
// private methods
/**
* Add a vertex to the lists of locations.
*
* @param location the approximate vertex location (in mesh coordinates, not
* null, unaffected)
* @param uOverride U value if the vertex has Y=0, otherwise null
* @return the index assigned to the new vertex (≥0)
*/
private int addVertex(Vector3f location, Float uOverride) {
float length = location.length();
locations.add(location.mult(radius / length));
uOverrides.add(uOverride);
assert locations.size() == uOverrides.size();
int result = nextVertexIndex;
++nextVertexIndex;
return result;
}
/**
* Convert 3-D Cartesian coordinates to latitude.
*
* @param input the location to transform (y = distance east of the plane of
* the zero meridian, z=distance north of the equatorial plane, not null,
* unaffected)
* @return the north latitude in (in radians)
*/
private static float latitude(Vector3f input) {
float result;
float length = input.length();
if (length > 0f) {
result = (float) Math.asin(input.z / length);
} else {
result = 0f;
}
return result;
}
/**
* Convert 3-D Cartesian coordinates to longitude.
*
* @param input the location to transform (y = distance east of the plane of
* the zero meridian, z=distance north of the equatorial plane, not null,
* unaffected)
* @return the west longitude (in radians)
*/
private static float longitude(Vector3f input) {
float result;
if (input.x != 0f || input.y != 0f) {
result = -FastMath.atan2(input.y, input.x);
} else {
result = 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);
Float middleUOverride = null;
if (middleLocation.y == 0f) {
middleUOverride = uOverrides.get(p1);
assert uOverrides.get(p2).equals(middleUOverride);
} else {
assert uOverrides.get(p1) == null || uOverrides.get(p2) == null;
}
// addVertex() scales the midpoint location to the sphere's surface.
int newIndex = addVertex(middleLocation, middleUOverride);
// Add the new vertex to the midpoint cache.
midpointCache.put(key, newIndex);
return newIndex;
}
}