Merge pull request #672 from Saucistophe/master

Refactored Cylinder
fix-456
empirephoenix 8 years ago committed by GitHub
commit 25570ba4b3
  1. 366
      jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java
  2. 78
      jme3-core/src/test/java/com/jme3/scene/ShapeGeometryTest.java

@ -40,11 +40,8 @@ import com.jme3.math.FastMath;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh; import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import static com.jme3.util.BufferUtils.*;
import java.io.IOException; import java.io.IOException;
import java.nio.FloatBuffer;
/** /**
* A simple cylinder, defined by it's height and radius. * A simple cylinder, defined by it's height and radius.
@ -127,10 +124,10 @@ public class Cylinder extends Mesh {
* mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need
* a suited distorted texture. * a suited distorted texture.
* *
* @param axisSamples * @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so
* Number of triangle samples along the axis. * that, for instance, 4 samples mean the cylinder will be made of 3 segments.
* @param radialSamples * @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the
* Number of triangle samples along the radial. * cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles.
* @param radius * @param radius
* The radius of the cylinder. * The radius of the cylinder.
* @param height * @param height
@ -201,194 +198,225 @@ public class Cylinder extends Mesh {
/** /**
* Rebuilds the cylinder based on a new set of parameters. * Rebuilds the cylinder based on a new set of parameters.
* *
* @param axisSamples the number of samples along the axis. * @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so
* @param radialSamples the number of samples around the radial. * that, for instance, 4 samples mean the cylinder will be made of 3 segments.
* @param radius the radius of the bottom of the cylinder. * @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the
* @param radius2 the radius of the top of the cylinder. * cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles.
* @param topRadius the radius of the top of the cylinder.
* @param bottomRadius the radius of the bottom of the cylinder.
* @param height the cylinder's height. * @param height the cylinder's height.
* @param closed should the cylinder have top and bottom surfaces. * @param closed should the cylinder have top and bottom surfaces.
* @param inverted is the cylinder is meant to be viewed from the inside. * @param inverted is the cylinder is meant to be viewed from the inside.
*/ */
public void updateGeometry(int axisSamples, int radialSamples, public void updateGeometry(int axisSamples, int radialSamples,
float radius, float radius2, float height, boolean closed, boolean inverted) { float topRadius, float bottomRadius, float height, boolean closed, boolean inverted) {
// Ensure there's at least two axis samples and 3 radial samples, and positive dimensions.
if( axisSamples < 2
|| radialSamples < 3
|| topRadius <= 0
|| bottomRadius <= 0
|| height <= 0 ) {
throw new IllegalArgumentException("Cylinders must have at least 2 axis samples and 3 radial samples, and positive dimensions.");
}
this.axisSamples = axisSamples; this.axisSamples = axisSamples;
this.radialSamples = radialSamples; this.radialSamples = radialSamples;
this.radius = radius; this.radius = bottomRadius;
this.radius2 = radius2; this.radius2 = topRadius;
this.height = height; this.height = height;
this.closed = closed; this.closed = closed;
this.inverted = inverted; this.inverted = inverted;
// VertexBuffer pvb = getBuffer(Type.Position); // Vertices : One per radial sample plus one duplicate for texture closing around the sides.
// VertexBuffer nvb = getBuffer(Type.Normal); int verticesCount = axisSamples * (radialSamples +1);
// VertexBuffer tvb = getBuffer(Type.TexCoord); // Triangles: Two per side rectangle, which is the product of numbers of samples.
axisSamples += (closed ? 2 : 0); int trianglesCount = axisSamples * radialSamples * 2 ;
if( closed ) {
// Vertices // If there are caps, add two additional rims and two summits.
int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0); verticesCount += 2 + 2 * (radialSamples +1);
// Add one triangle per radial sample, twice, to form the caps.
setBuffer(Type.Position, 3, createVector3Buffer(getFloatBuffer(Type.Position), vertCount)); trianglesCount += 2 * radialSamples ;
}
// Normals
setBuffer(Type.Normal, 3, createVector3Buffer(getFloatBuffer(Type.Normal), vertCount));
// Texture co-ordinates
setBuffer(Type.TexCoord, 2, createVector2Buffer(vertCount));
int triCount = ((closed ? 2 : 0) + 2 * (axisSamples - 1)) * radialSamples; // Compute the points along a unit circle:
float[][] circlePoints = new float[radialSamples+1][2];
for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) {
float angle = FastMath.TWO_PI / radialSamples * circlePoint;
circlePoints[circlePoint][0] = FastMath.cos(angle);
circlePoints[circlePoint][1] = FastMath.sin(angle);
}
// Add an additional point for closing the texture around the side of the cylinder.
circlePoints[radialSamples][0] = circlePoints[0][0];
circlePoints[radialSamples][1] = circlePoints[0][1];
setBuffer(Type.Index, 3, createShortBuffer(getShortBuffer(Type.Index), 3 * triCount)); // Calculate normals.
//
// generate geometry // A---------B
float inverseRadial = 1.0f / radialSamples; // \ |
float inverseAxisLess = 1.0f / (closed ? axisSamples - 3 : axisSamples - 1); // \ |
float inverseAxisLessTexture = 1.0f / (axisSamples - 1); // \ |
float halfHeight = 0.5f * height; // D-----C
//
// Generate points on the unit circle to be used in computing the mesh // Let be B and C the top and bottom points of the axis, and A and D the top and bottom edges.
// points on a cylinder slice. // The normal in A and D is simply orthogonal to AD, which means we can get it once per sample.
float[] sin = new float[radialSamples + 1]; //
float[] cos = new float[radialSamples + 1]; Vector3f[] circleNormals = new Vector3f[radialSamples+1];
for (int circlePoint = 0; circlePoint < radialSamples+1; circlePoint++) {
for (int radialCount = 0; radialCount < radialSamples; radialCount++) { // The normal is the orthogonal to the side, which can be got without trigonometry.
float angle = FastMath.TWO_PI * inverseRadial * radialCount; // The edge direction is oriented so that it goes up by Height, and out by the radius difference; let's use
cos[radialCount] = FastMath.cos(angle); // those values in reverse order.
sin[radialCount] = FastMath.sin(angle); Vector3f normal = new Vector3f(height * circlePoints[circlePoint][0], height * circlePoints[circlePoint][1], bottomRadius - topRadius );
circleNormals[circlePoint] = normal.normalizeLocal();
} }
sin[radialSamples] = sin[0];
cos[radialSamples] = cos[0]; float[] vertices = new float[verticesCount * 3];
float[] normals = new float[verticesCount * 3];
// calculate normals float[] textureCoords = new float[verticesCount * 2];
Vector3f[] vNormals = null; int currentIndex = 0;
Vector3f vNormal = Vector3f.UNIT_Z;
// Add a circle of points for each axis sample.
if ((height != 0.0f) && (radius != radius2)) { for(int axisSample = 0; axisSample < axisSamples; axisSample++ ) {
vNormals = new Vector3f[radialSamples]; float currentHeight = -height / 2 + height * axisSample / (axisSamples-1);
Vector3f vHeight = Vector3f.UNIT_Z.mult(height); float currentRadius = bottomRadius + (topRadius - bottomRadius) * axisSample / (axisSamples-1);
Vector3f vRadial = new Vector3f();
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++) {
for (int radialCount = 0; radialCount < radialSamples; radialCount++) { // Position, by multipliying the position on a unit circle with the current radius.
vRadial.set(cos[radialCount], sin[radialCount], 0.0f); vertices[currentIndex*3] = circlePoints[circlePoint][0] * currentRadius;
Vector3f vRadius = vRadial.mult(radius); vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * currentRadius;
Vector3f vRadius2 = vRadial.mult(radius2); vertices[currentIndex*3 +2] = currentHeight;
Vector3f vMantle = vHeight.subtract(vRadius2.subtract(vRadius));
Vector3f vTangent = vRadial.cross(Vector3f.UNIT_Z); // Normal
vNormals[radialCount] = vMantle.cross(vTangent).normalize(); Vector3f currentNormal = circleNormals[circlePoint];
normals[currentIndex*3] = currentNormal.x;
normals[currentIndex*3+1] = currentNormal.y;
normals[currentIndex*3+2] = currentNormal.z;
// Texture
// The X is the angular position of the point.
textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
// Depending on whether there is a cap, the Y is either the height scaled to [0,1], or the radii of
// the cap count as well.
if (closed)
textureCoords[currentIndex *2 +1] = (bottomRadius + height / 2 + currentHeight) / (bottomRadius + height + topRadius);
else
textureCoords[currentIndex *2 +1] = height / 2 + currentHeight;
currentIndex++;
} }
} }
// If closed, add duplicate rims on top and bottom, with normals facing up and down.
if (closed) {
// Bottom
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++) {
vertices[currentIndex*3] = circlePoints[circlePoint][0] * bottomRadius;
vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * bottomRadius;
vertices[currentIndex*3 +2] = -height/2;
FloatBuffer nb = getFloatBuffer(Type.Normal); normals[currentIndex*3] = 0;
FloatBuffer pb = getFloatBuffer(Type.Position); normals[currentIndex*3+1] = 0;
FloatBuffer tb = getFloatBuffer(Type.TexCoord); normals[currentIndex*3+2] = -1;
// generate the cylinder itself
Vector3f tempNormal = new Vector3f();
for (int axisCount = 0, i = 0; axisCount < axisSamples; axisCount++, i++) {
float axisFraction;
float axisFractionTexture;
int topBottom = 0;
if (!closed) {
axisFraction = axisCount * inverseAxisLess; // in [0,1]
axisFractionTexture = axisFraction;
} else {
if (axisCount == 0) {
topBottom = -1; // bottom
axisFraction = 0;
axisFractionTexture = inverseAxisLessTexture;
} else if (axisCount == axisSamples - 1) {
topBottom = 1; // top
axisFraction = 1;
axisFractionTexture = 1 - inverseAxisLessTexture;
} else {
axisFraction = (axisCount - 1) * inverseAxisLess;
axisFractionTexture = axisCount * inverseAxisLessTexture;
}
}
// compute center of slice textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
float z = -halfHeight + height * axisFraction; textureCoords[currentIndex *2 +1] = bottomRadius / (bottomRadius + height + topRadius);
Vector3f sliceCenter = new Vector3f(0, 0, z);
currentIndex++;
// compute slice vertices with duplication at end point
int save = i;
for (int radialCount = 0; radialCount < radialSamples; radialCount++, i++) {
float radialFraction = radialCount * inverseRadial; // in [0,1)
tempNormal.set(cos[radialCount], sin[radialCount], 0.0f);
if (vNormals != null) {
vNormal = vNormals[radialCount];
} else if (radius == radius2) {
vNormal = tempNormal;
}
if (topBottom == 0) {
if (!inverted)
nb.put(vNormal.x).put(vNormal.y).put(vNormal.z);
else
nb.put(-vNormal.x).put(-vNormal.y).put(-vNormal.z);
} else {
nb.put(0).put(0).put(topBottom * (inverted ? -1 : 1));
}
tempNormal.multLocal((radius - radius2) * axisFraction + radius2)
.addLocal(sliceCenter);
pb.put(tempNormal.x).put(tempNormal.y).put(tempNormal.z);
tb.put((inverted ? 1 - radialFraction : radialFraction))
.put(axisFractionTexture);
} }
// Top
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++) {
vertices[currentIndex*3] = circlePoints[circlePoint][0] * topRadius;
vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * topRadius;
vertices[currentIndex*3 +2] = height/2;
BufferUtils.copyInternalVector3(pb, save, i); normals[currentIndex*3] = 0;
BufferUtils.copyInternalVector3(nb, save, i); normals[currentIndex*3+1] = 0;
normals[currentIndex*3+2] = 1;
tb.put((inverted ? 0.0f : 1.0f)) textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
.put(axisFractionTexture); textureCoords[currentIndex *2 +1] = (bottomRadius + height) / (bottomRadius + height + topRadius);
}
if (closed) { currentIndex++;
pb.put(0).put(0).put(-halfHeight); // bottom center }
nb.put(0).put(0).put(-1 * (inverted ? -1 : 1));
tb.put(0.5f).put(0); // Add the centers of the caps.
pb.put(0).put(0).put(halfHeight); // top center vertices[currentIndex*3] = 0;
nb.put(0).put(0).put(1 * (inverted ? -1 : 1)); vertices[currentIndex*3 +1] = 0;
tb.put(0.5f).put(1); vertices[currentIndex*3 +2] = -height/2;
normals[currentIndex*3] = 0;
normals[currentIndex*3+1] = 0;
normals[currentIndex*3+2] = -1;
textureCoords[currentIndex *2] = 0.5f;
textureCoords[currentIndex *2+1] = 0f;
currentIndex++;
vertices[currentIndex*3] = 0;
vertices[currentIndex*3 +1] = 0;
vertices[currentIndex*3 +2] = height/2;
normals[currentIndex*3] = 0;
normals[currentIndex*3+1] = 0;
normals[currentIndex*3+2] = 1;
textureCoords[currentIndex *2] = 0.5f;
textureCoords[currentIndex *2+1] = 1f;
} }
IndexBuffer ib = getIndexBuffer(); // Add the triangles indexes.
int index = 0; short[] indices = new short[trianglesCount * 3];
// Connectivity currentIndex = 0;
for (int axisCount = 0, axisStart = 0; axisCount < axisSamples - 1; axisCount++) { for (short axisSample = 0; axisSample < axisSamples - 1; axisSample++) {
int i0 = axisStart; for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) {
int i1 = i0 + 1; indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint);
axisStart += radialSamples + 1; indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1);
int i2 = axisStart; indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint);
int i3 = i2 + 1;
for (int i = 0; i < radialSamples; i++) { indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint);
if (closed && axisCount == 0) { indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1);
if (!inverted) { indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint + 1);
ib.put(index++, i0++); }
ib.put(index++, vertCount - 2); }
ib.put(index++, i1++); // Add caps if needed.
} else { if(closed) {
ib.put(index++, i0++); short bottomCapIndex = (short) (verticesCount - 2);
ib.put(index++, i1++); short topCapIndex = (short) (verticesCount - 1);
ib.put(index++, vertCount - 2);
} int bottomRowOffset = (axisSamples) * (radialSamples +1 );
} else if (closed && axisCount == axisSamples - 2) { int topRowOffset = (axisSamples+1) * (radialSamples +1 );
ib.put(index++, i2++);
ib.put(index++, inverted ? vertCount - 1 : i3++); for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) {
ib.put(index++, inverted ? i3++ : vertCount - 1); indices[currentIndex++] = (short) (bottomRowOffset + circlePoint +1);
} else { indices[currentIndex++] = (short) (bottomRowOffset + circlePoint);
ib.put(index++, i0++); indices[currentIndex++] = bottomCapIndex;
ib.put(index++, inverted ? i2 : i1);
ib.put(index++, inverted ? i1 : i2);
ib.put(index++, i1++); indices[currentIndex++] = (short) (topRowOffset + circlePoint);
ib.put(index++, inverted ? i2++ : i3++); indices[currentIndex++] = (short) (topRowOffset + circlePoint +1);
ib.put(index++, inverted ? i3++ : i2++); indices[currentIndex++] = topCapIndex;
}
} }
} }
// If inverted, the triangles and normals are all reverted.
if (inverted) {
for (int i = 0; i < indices.length / 2; i++) {
short temp = indices[i];
indices[i] = indices[indices.length - 1 - i];
indices[indices.length - 1 - i] = temp;
}
for(int i = 0; i< normals.length; i++) {
normals[i] = -normals[i];
}
}
// Fill in the buffers.
setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(textureCoords));
setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(indices));
updateBound(); updateBound();
setStatic(); setStatic();
} }
@ -418,6 +446,4 @@ public class Cylinder extends Mesh {
capsule.write(closed, "closed", false); capsule.write(closed, "closed", false);
capsule.write(inverted, "inverted", false); capsule.write(inverted, "inverted", false);
} }
} }

@ -0,0 +1,78 @@
/*
* Copyright (c) 2009-2017 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.scene;
import com.jme3.collision.CollisionResults;
import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.shape.Cylinder;
import java.util.Random;
import org.junit.Test;
/**
* Ensures that geometries behave correctly, by casting rays and ensure they don't break.
*
* @author Christophe Carpentier
*/
public class ShapeGeometryTest {
protected static final int NUMBER_OF_TRIES = 1000;
@Test
public void testCylinders() {
Random random = new Random();
// Create a cylinder, cast a random ray, and ensure everything goes well.
Node scene = new Node("Scene Node");
for (int i = 0; i < NUMBER_OF_TRIES; i++) {
scene.detachAllChildren();
Cylinder cylinder = new Cylinder(2, 8, 1, 1, true);
Geometry geometry = new Geometry("cylinder", cylinder);
geometry.rotate(FastMath.HALF_PI, 0, 0);
scene.attachChild(geometry);
// Cast a random ray, and count successes and IndexOutOfBoundsExceptions.
Vector3f randomPoint = new Vector3f(random.nextFloat(), random.nextFloat(), random.nextFloat());
Vector3f randomDirection = new Vector3f(random.nextFloat(), random.nextFloat(), random.nextFloat());
randomDirection.normalizeLocal();
Ray ray = new Ray(randomPoint, randomDirection);
CollisionResults collisionResults = new CollisionResults();
// If the geometry is invalid, this should throw various exceptions.
scene.collideWith(ray, collisionResults);
}
}
}
Loading…
Cancel
Save