From ebaad20f2f03fa3c96fc45e39dc5c8d53df6c6f5 Mon Sep 17 00:00:00 2001 From: stophe Date: Sun, 18 Jun 2017 23:56:14 +0200 Subject: [PATCH] Added a unit test and fixed indentation. --- .../java/com/jme3/scene/shape/Cylinder.java | 397 +++++++++--------- .../com/jme3/scene/ShapeGeometryTest.java | 78 ++++ 2 files changed, 269 insertions(+), 206 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/scene/ShapeGeometryTest.java diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java b/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java index c46a81b4f..97dd70f30 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java @@ -125,9 +125,9 @@ public class Cylinder extends Mesh { * a suited distorted texture. * * @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so - * that, for instance, 4 samples mean the cylinder will be made of 3 segments. + * that, for instance, 4 samples mean the cylinder will be made of 3 segments. * @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the - * cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles. + * cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles. * @param radius * The radius of the cylinder. * @param height @@ -199,27 +199,27 @@ public class Cylinder extends Mesh { * Rebuilds the cylinder based on a new set of parameters. * * @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so - * that, for instance, 4 samples mean the cylinder will be made of 3 segments. + * that, for instance, 4 samples mean the cylinder will be made of 3 segments. * @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the - * cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles. + * 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 bottomRadius the radius of the bottom of the cylinder. * @param height the cylinder's height. * @param closed should the cylinder have top and bottom surfaces. * @param inverted is the cylinder is meant to be viewed from the inside. */ public void updateGeometry(int axisSamples, int radialSamples, - 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; + 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.radialSamples = radialSamples; this.radius = bottomRadius; this.radius2 = topRadius; @@ -229,209 +229,194 @@ public class Cylinder extends Mesh { // Vertices : One per radial sample plus one duplicate for texture closing around the sides. int verticesCount = axisSamples * (radialSamples +1); - // Triangles: Two per side rectangle, which is the product of numbers of samples. - int trianglesCount = axisSamples * radialSamples * 2 ; - if( closed ) - { - // If there are caps, add two additional rims and two summits. - verticesCount += 2 + 2 * (radialSamples +1); - // Add one triangle per radial sample, twice, to form the caps. - trianglesCount += 2 * radialSamples ; - } - - // Compute the points along a unit circle: - float[][] circlePoints = new float[radialSamples+1][2]; - for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) - { + // Triangles: Two per side rectangle, which is the product of numbers of samples. + int trianglesCount = axisSamples * radialSamples * 2 ; + if( closed ) { + // If there are caps, add two additional rims and two summits. + verticesCount += 2 + 2 * (radialSamples +1); + // Add one triangle per radial sample, twice, to form the caps. + trianglesCount += 2 * 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]; + // 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]; - + // Calculate normals. - // - // A---------B - // \ | - // \ | - // \ | - // D-----C - // - // Let be B and C the top and bottom points of the axis, and A and D the top and bottom edges. - // The normal in A and D is simply orthogonal to AD, which means we can get it once per sample. - // - Vector3f[] circleNormals = new Vector3f[radialSamples+1]; - for (int circlePoint = 0; circlePoint < radialSamples+1; circlePoint++) - { + // + // A---------B + // \ | + // \ | + // \ | + // D-----C + // + // Let be B and C the top and bottom points of the axis, and A and D the top and bottom edges. + // The normal in A and D is simply orthogonal to AD, which means we can get it once per sample. + // + Vector3f[] circleNormals = new Vector3f[radialSamples+1]; + for (int circlePoint = 0; circlePoint < radialSamples+1; circlePoint++) { // The normal is the orthogonal to the side, which can be got without trigonometry. - // The edge direction is oriented so that it goes up by Height, and out by the radius difference; let's use - // those values in reverse order. - Vector3f normal = new Vector3f(height * circlePoints[circlePoint][0], height * circlePoints[circlePoint][1], bottomRadius - topRadius ); - circleNormals[circlePoint] = normal.normalizeLocal(); + // The edge direction is oriented so that it goes up by Height, and out by the radius difference; let's use + // those values in reverse order. + Vector3f normal = new Vector3f(height * circlePoints[circlePoint][0], height * circlePoints[circlePoint][1], bottomRadius - topRadius ); + circleNormals[circlePoint] = normal.normalizeLocal(); } - float[] vertices = new float[verticesCount * 3]; - float[] normals = new float[verticesCount * 3]; - float[] textureCoords = new float[verticesCount * 2]; - int currentIndex = 0; - - // Add a circle of points for each axis sample. - for(int axisSample = 0; axisSample < axisSamples; axisSample++ ) - { - float currentHeight = -height / 2 + height * axisSample / (axisSamples-1); - float currentRadius = bottomRadius + (topRadius - bottomRadius) * axisSample / (axisSamples-1); - - for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++) - { - // Position, by multipliying the position on a unit circle with the current radius. - vertices[currentIndex*3] = circlePoints[circlePoint][0] * currentRadius; - vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * currentRadius; - vertices[currentIndex*3 +2] = currentHeight; - - // Normal - 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; - - normals[currentIndex*3] = 0; - normals[currentIndex*3+1] = 0; - normals[currentIndex*3+2] = -1; - - textureCoords[currentIndex *2] = (float) circlePoint / radialSamples; - textureCoords[currentIndex *2 +1] = bottomRadius / (bottomRadius + height + topRadius); - - currentIndex++; - } - // 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; - - normals[currentIndex*3] = 0; - normals[currentIndex*3+1] = 0; - normals[currentIndex*3+2] = 1; - - textureCoords[currentIndex *2] = (float) circlePoint / radialSamples; - textureCoords[currentIndex *2 +1] = (bottomRadius + height) / (bottomRadius + height + topRadius); - - currentIndex++; - } - - // Add the centers of the caps. - 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] = 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; + float[] vertices = new float[verticesCount * 3]; + float[] normals = new float[verticesCount * 3]; + float[] textureCoords = new float[verticesCount * 2]; + int currentIndex = 0; + + // Add a circle of points for each axis sample. + for(int axisSample = 0; axisSample < axisSamples; axisSample++ ) { + float currentHeight = -height / 2 + height * axisSample / (axisSamples-1); + float currentRadius = bottomRadius + (topRadius - bottomRadius) * axisSample / (axisSamples-1); + + for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++) { + // Position, by multipliying the position on a unit circle with the current radius. + vertices[currentIndex*3] = circlePoints[circlePoint][0] * currentRadius; + vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * currentRadius; + vertices[currentIndex*3 +2] = currentHeight; + + // Normal + 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; + + normals[currentIndex*3] = 0; + normals[currentIndex*3+1] = 0; + normals[currentIndex*3+2] = -1; + + textureCoords[currentIndex *2] = (float) circlePoint / radialSamples; + textureCoords[currentIndex *2 +1] = bottomRadius / (bottomRadius + height + topRadius); + + currentIndex++; + } + // 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; + + normals[currentIndex*3] = 0; + normals[currentIndex*3+1] = 0; + normals[currentIndex*3+2] = 1; + + textureCoords[currentIndex *2] = (float) circlePoint / radialSamples; + textureCoords[currentIndex *2 +1] = (bottomRadius + height) / (bottomRadius + height + topRadius); + + currentIndex++; + } + + // Add the centers of the caps. + 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] = 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; } - // Add the triangles indexes. + // Add the triangles indexes. short[] indices = new short[trianglesCount * 3]; - currentIndex = 0; - for (short axisSample = 0; axisSample < axisSamples - 1; axisSample++) - { - for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) - { - indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint); - indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1); - indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint); - - indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint); - indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1); - indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint + 1); - } - } - // Add caps if needed. - if(closed) - { - short bottomCapIndex = (short) (verticesCount - 2); - short topCapIndex = (short) (verticesCount - 1); - - int bottomRowOffset = (axisSamples) * (radialSamples +1 ); - int topRowOffset = (axisSamples+1) * (radialSamples +1 ); - - for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) - { - indices[currentIndex++] = (short) (bottomRowOffset + circlePoint +1); - indices[currentIndex++] = (short) (bottomRowOffset + circlePoint); - indices[currentIndex++] = bottomCapIndex; - - - indices[currentIndex++] = (short) (topRowOffset + circlePoint); - indices[currentIndex++] = (short) (topRowOffset + circlePoint +1); - 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)); + currentIndex = 0; + for (short axisSample = 0; axisSample < axisSamples - 1; axisSample++) { + for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) { + indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint); + indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1); + indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint); + + indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint); + indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1); + indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint + 1); + } + } + // Add caps if needed. + if(closed) { + short bottomCapIndex = (short) (verticesCount - 2); + short topCapIndex = (short) (verticesCount - 1); + + int bottomRowOffset = (axisSamples) * (radialSamples +1 ); + int topRowOffset = (axisSamples+1) * (radialSamples +1 ); + + for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++) { + indices[currentIndex++] = (short) (bottomRowOffset + circlePoint +1); + indices[currentIndex++] = (short) (bottomRowOffset + circlePoint); + indices[currentIndex++] = bottomCapIndex; + + + indices[currentIndex++] = (short) (topRowOffset + circlePoint); + indices[currentIndex++] = (short) (topRowOffset + circlePoint +1); + 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(); setStatic(); } diff --git a/jme3-core/src/test/java/com/jme3/scene/ShapeGeometryTest.java b/jme3-core/src/test/java/com/jme3/scene/ShapeGeometryTest.java new file mode 100644 index 000000000..af20a134d --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/scene/ShapeGeometryTest.java @@ -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); + } + } +}