[Committed]angle parameter in shape constructors

Overview

I wanted to add a quarter-tube and half-tube to some scene I was experimenting with but found that tube shapes can't be parametrized with angles. I modified the tube shape and also the disk shape to accept angle parameters and here's the result:






Changes
The default assumption was that the shape will wrap around on itself, so the first vertices were to be connected to the last ones. I had to add another round of vertices so the shape goes around up to the desired angle. This is necessary when the angle is less than 2xPi but if the angle is 2xPi, these newly added vertices will be the same as the first ones, which means they would be redundant in that case, but it makes the shape general for any angle. This applies to both tubes and disks.

Also, for the tube shape, I had to close the introduced edges as the tube gets cut along its axis. I connected the right vertices from the top and bottom surfaces without adding any more vertices. For now, these vertical edges have no texture coordinates since I didn't add more vertices for them.

Changed Code
Disk.java


import java.io.IOException;

import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.scene.TexCoords;
import com.jme.scene.TriMesh;
import com.jme.util.export.InputCapsule;
import com.jme.util.export.JMEExporter;
import com.jme.util.export.JMEImporter;
import com.jme.util.export.OutputCapsule;
import com.jme.util.geom.BufferUtils;

/**
 * A flat discus, defined by it's radius.
 *
 * @author Mark Powell
 * @version $Revision: 4091 $, $Date: 2009-01-21 21:01:20 +0200 (Wed, 21 Jan 2009) $
 */
public class Disk extends TriMesh {

    private static final long serialVersionUID = 1L;

    private int shellSamples;

    private int radialSamples;

    private float radius;
    
    private float arcAngle;

    public Disk() {
    }

    /**
     * Creates a flat disk (circle) at the origin flat along the Z. Usually, a
     * higher sample number creates a better looking cylinder, but at the cost
     * of more vertex information.
     *
     * @param name
     *            The name of the disk.
     * @param shellSamples
     *            The number of shell samples.
     * @param radialSamples
     *            The number of radial samples.
     * @param radius
     *            The radius of the disk.
     */
    public Disk(String name, int shellSamples, int radialSamples, float radius) {
        super(name);
        updateGeometry(shellSamples, radialSamples, radius, FastMath.TWO_PI);
    }
    
    public Disk(String name, int shellSamples, int radialSamples, float radius, float arcAngle) {
        super(name);
        updateGeometry(shellSamples, radialSamples, radius, arcAngle);
    }
    
    public float getArcAngle() {
       return arcAngle;
    }

    public int getRadialSamples() {
        return radialSamples;
    }

    public float getRadius() {
        return radius;
    }

    public int getShellSamples() {
        return shellSamples;
    }

    public void read(JMEImporter e) throws IOException {
        super.read(e);
        InputCapsule capsule = e.getCapsule(this);
        shellSamples = capsule.readInt("shellSamples", 0);
        radialSamples = capsule.readInt("radialSamples", 0);
        radius = capsule.readFloat("raidus", 0);
        arcAngle = capsule.readFloat("arcAngle", FastMath.TWO_PI);
    }

    /**
     * Rebuild this disk based on a new set of parameters.
     *
     * @param shellSamples the number of shell samples.
     * @param radialSamples the number of radial samples.
     * @param radius the radius of the disk.
     * @param arc angle the disk is to cover.
     */
    public void updateGeometry(int shellSamples, int radialSamples, float radius, float arcAngle) {
        this.shellSamples = shellSamples;
        this.radialSamples = radialSamples;
        this.radius = radius;
        this.arcAngle = arcAngle;
        int shellLess = shellSamples - 1;
        // Allocate vertices
        setVertexCount(1 + (radialSamples+1) * shellLess);
        setVertexBuffer(BufferUtils.createVector3Buffer(getVertexCount()));
        setNormalBuffer(BufferUtils.createVector3Buffer(getVertexCount()));
        getTextureCoords().set(0, new TexCoords(BufferUtils.createVector3Buffer(getVertexCount())));
        setTriangleQuantity(radialSamples * (2 * shellLess - 1));
        setIndexBuffer(BufferUtils.createIntBuffer(3 * getTriangleCount()));
        
        // generate geometry
        // center of disk
        getVertexBuffer().put(0).put(0).put(0);
        
        for (int x = 0; x < getVertexCount(); x++) {
            getNormalBuffer().put(0).put(0).put(1);
        }

        getTextureCoords().get(0).coords.put(.5f).put(.5f);
        float inverseShellLess = 1.0f / shellLess;
        float inverseRadial = 1.0f / radialSamples;
        Vector3f radialFraction = new Vector3f();
        Vector2f texCoord = new Vector2f();
        for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
            float angle = arcAngle * inverseRadial * radialCount;
            float cos = FastMath.cos(angle);
            float sin = FastMath.sin(angle);
            Vector3f radial = new Vector3f(cos, sin, 0);
        
            for (int shellCount = 1; shellCount < shellSamples; shellCount++) {
                float fraction = inverseShellLess * shellCount; // in (0,R]
                radialFraction.set(radial).multLocal(fraction);
                int i = shellCount + shellLess * radialCount;
                texCoord.x = 0.5f * (1.0f + radialFraction.x);
                texCoord.y = 0.5f * (1.0f + radialFraction.y);
                BufferUtils.setInBuffer(texCoord, getTextureCoords().get(0).coords, i);
                radialFraction.multLocal(radius);
                BufferUtils.setInBuffer(radialFraction, getVertexBuffer(), i);
            }
        }
        
        // Generate connectivity
        int index = 0;
        for (int radialCount0 = 0, radialCount1 = 1; radialCount1 <= radialSamples; radialCount0 = radialCount1++) {
            getIndexBuffer().put(0);
            getIndexBuffer().put(1 + shellLess * radialCount0);
            getIndexBuffer().put(1 + shellLess * radialCount1);
            index += 3;
            for (int iS = 1; iS < shellLess; iS++, index += 6) {
                int i00 = iS + shellLess * radialCount0;
                int i01 = iS + shellLess * radialCount1;
                int i10 = i00 + 1;
                int i11 = i01 + 1;
                getIndexBuffer().put(i00);
                getIndexBuffer().put(i10);
                getIndexBuffer().put(i11);
                getIndexBuffer().put(i00);
                getIndexBuffer().put(i11);
                getIndexBuffer().put(i01);
            }
        }
    }

    public void write(JMEExporter e) throws IOException {
        super.write(e);
        OutputCapsule capsule = e.getCapsule(this);
        capsule.write(shellSamples, "shellSamples", 0);
        capsule.write(radialSamples, "radialSamples", 0);
        capsule.write(radius, "radius", 0);
        capsule.write(arcAngle, "arcAngle", 0);
    }

}



Tube.java


import java.io.IOException;

import com.jme.math.FastMath;
import com.jme.scene.TexCoords;
import com.jme.scene.TriMesh;
import com.jme.util.export.InputCapsule;
import com.jme.util.export.JMEExporter;
import com.jme.util.export.JMEImporter;
import com.jme.util.export.OutputCapsule;
import com.jme.util.export.Savable;
import com.jme.util.geom.BufferUtils;

/**
 *
 * @author Landei
 */
public class Tube extends TriMesh implements Savable {

   private static final long serialVersionUID = 1L;

   @Deprecated
   public static long getSerialVersionUID() {
      return serialVersionUID;
   }

   private int axisSamples;
   private int radialSamples;

   private float outerRadius;
   private float innerRadius;
   private float height;
   private float arcAngle;

   /**
    * Constructor meant for Savable use only.
    */
   public Tube() {
   }

   public Tube(String name, float outerRadius, float innerRadius,
         float height) {
      this(name, outerRadius, innerRadius, height, 2, 20, FastMath.TWO_PI);
   }

   public Tube(String name, float outerRadius, float innerRadius,
         float height, float arcAngle) {
      this(name, outerRadius, innerRadius, height, 2, 20 * (int) (FastMath
            .ceil(arcAngle * FastMath.INV_TWO_PI)), arcAngle);
   }

   public Tube(String name, float outerRadius, float innerRadius,
         float height, int axisSamples, int radialSamples, float arcAngle) {
      super(name);
      updateGeometry(outerRadius, innerRadius, height, axisSamples,
            radialSamples, arcAngle);
   }

   public float getArcAngle() {
      return arcAngle;
   }

   public int getAxisSamples() {
      return axisSamples;
   }

   public float getHeight() {
      return height;
   }

   public float getInnerRadius() {
      return innerRadius;
   }

   public float getOuterRadius() {
      return outerRadius;
   }

   public int getRadialSamples() {
      return radialSamples;
   }

   @Override
   public void read(JMEImporter e) throws IOException {
      super.read(e);
      InputCapsule capsule = e.getCapsule(this);
      int axisSamples = capsule.readInt("axisSamples", 0);
      int radialSamples = capsule.readInt("radialSamples", 0);
      float outerRadius = capsule.readFloat("outerRadius", 0);
      float innerRadius = capsule.readFloat("innerRadius", 0);
      float height = capsule.readFloat("height", 0);
      float angle = capsule.readFloat("angle", FastMath.TWO_PI);
      updateGeometry(outerRadius, innerRadius, height, axisSamples,
            radialSamples, angle);
   }

   private void setGeometryData() {
      float inverseRadial = 1.0f / radialSamples;
      float axisStep = height / axisSamples;
      float axisTextureStep = 1.0f / axisSamples;
      float halfHeight = 0.5f * height;
      float innerOuterRatio = innerRadius / outerRadius;
      float[] sin = new float[radialSamples + 1];
      float[] cos = new float[radialSamples + 1];

      for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
         float angle = arcAngle * inverseRadial * radialCount;
         cos[radialCount] = FastMath.cos(angle);
         sin[radialCount] = FastMath.sin(angle);
      }

      // outer cylinder
      for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
         for (int axisCount = 0; axisCount <= axisSamples; axisCount++) {
            getVertexBuffer().put(cos[radialCount] * outerRadius).put(
                  axisStep * axisCount - halfHeight).put(
                  sin[radialCount] * outerRadius);
            getNormalBuffer().put(cos[radialCount]).put(0).put(
                  sin[radialCount]);
            getTextureCoords(0).coords.put(radialCount * inverseRadial)
                  .put(axisTextureStep * axisCount);
         }
      }
      // inner cylinder
      for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
         for (int axisCount = 0; axisCount <= axisSamples; axisCount++) {
            getVertexBuffer().put(cos[radialCount] * innerRadius).put(
                  axisStep * axisCount - halfHeight).put(
                  sin[radialCount] * innerRadius);
            getNormalBuffer().put(-cos[radialCount]).put(0).put(
                  -sin[radialCount]);
            getTextureCoords(0).coords.put(radialCount * inverseRadial)
                  .put(axisTextureStep * axisCount);
         }
      }
      // bottom edge
      for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
         getVertexBuffer().put(cos[radialCount] * outerRadius).put(
               -halfHeight).put(sin[radialCount] * outerRadius);
         getVertexBuffer().put(cos[radialCount] * innerRadius).put(
               -halfHeight).put(sin[radialCount] * innerRadius);
         getNormalBuffer().put(0).put(-1).put(0);
         getNormalBuffer().put(0).put(-1).put(0);
         getTextureCoords(0).coords.put(0.5f + 0.5f * cos[radialCount]).put(
               0.5f + 0.5f * sin[radialCount]);
         getTextureCoords(0).coords.put(
               0.5f + innerOuterRatio * 0.5f * cos[radialCount]).put(
               0.5f + innerOuterRatio * 0.5f * sin[radialCount]);
      }
      // top edge
      for (int radialCount = 0; radialCount <= radialSamples; radialCount++) {
         getVertexBuffer().put(cos[radialCount] * outerRadius).put(
               halfHeight).put(sin[radialCount] * outerRadius);
         getVertexBuffer().put(cos[radialCount] * innerRadius).put(
               halfHeight).put(sin[radialCount] * innerRadius);
         getNormalBuffer().put(0).put(1).put(0);
         getNormalBuffer().put(0).put(1).put(0);
         getTextureCoords(0).coords.put(0.5f + 0.5f * cos[radialCount]).put(
               0.5f + 0.5f * sin[radialCount]);
         getTextureCoords(0).coords.put(
               0.5f + innerOuterRatio * 0.5f * cos[radialCount]).put(
               0.5f + innerOuterRatio * 0.5f * sin[radialCount]);
      }

   }

   private void setIndexData() {
      int axisSamplesPlusOne = axisSamples + 1;
      int innerCylinder = axisSamplesPlusOne * (radialSamples + 1);
      int bottomEdge = 2 * innerCylinder;
      int topEdge = bottomEdge + 2 * (radialSamples + 1);
      // outer cylinder
      for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
         for (int axisCount = 0; axisCount < axisSamples; axisCount++) {
            int index0 = axisCount + axisSamplesPlusOne * radialCount;
            int index1 = index0 + 1;
            int index2 = index0 + axisSamplesPlusOne;
            int index3 = index2 + 1;
            getIndexBuffer().put(index0).put(index1).put(index2);
            getIndexBuffer().put(index1).put(index3).put(index2);
         }
      }

      // inner cylinder
      for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
         for (int axisCount = 0; axisCount < axisSamples; axisCount++) {
            int index0 = innerCylinder + axisCount + axisSamplesPlusOne
                  * radialCount;
            int index1 = index0 + 1;
            int index2 = index0 + axisSamplesPlusOne;
            int index3 = index2 + 1;
            getIndexBuffer().put(index0).put(index2).put(index1);
            getIndexBuffer().put(index1).put(index2).put(index3);
         }
      }

      // bottom edge
      for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
         int index0 = bottomEdge + 2 * radialCount;
         int index1 = index0 + 1;
         int index2 = index1 + 1;
         int index3 = index2 + 1;
         getIndexBuffer().put(index0).put(index2).put(index1);
         getIndexBuffer().put(index1).put(index2).put(index3);
      }

      // top edge
      for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
         int index0 = topEdge + 2 * radialCount;
         int index1 = index0 + 1;
         int index2 = index1 + 1;
         int index3 = index2 + 1;
         getIndexBuffer().put(index0).put(index1).put(index2);
         getIndexBuffer().put(index1).put(index3).put(index2);
      }

      // vertical edge0
      int bottomIndex0 = 0;
      int bottomIndex1 = innerCylinder;
      int topIndex0 = axisSamples;
      int topIndex1 = bottomIndex1 + axisSamples;
      getIndexBuffer().put(bottomIndex0).put(bottomIndex1).put(topIndex0);
      getIndexBuffer().put(bottomIndex1).put(topIndex1).put(topIndex0);

      // vertical edge1
      bottomIndex0 = innerCylinder - (axisSamples + 1);
      bottomIndex1 = 2 * innerCylinder - (axisSamples + 1);
      topIndex0 = bottomIndex0 + axisSamples;
      topIndex1 = bottomIndex1 + axisSamples;
      getIndexBuffer().put(bottomIndex0).put(bottomIndex1).put(topIndex0);
      getIndexBuffer().put(bottomIndex1).put(topIndex1).put(topIndex0);
   }

   public void updateGeometry(float outerRadius, float innerRadius,
         float height, int axisSamples, int radialSamples, float angle) {
      this.outerRadius = outerRadius;
      this.innerRadius = innerRadius;
      this.height = height;
      this.axisSamples = axisSamples;
      this.radialSamples = radialSamples;
      this.arcAngle = angle;
      setVertexCount(2 * (axisSamples + 1) * (radialSamples + 1)
            + (radialSamples + 1) * 4);
      setVertexBuffer(BufferUtils.createVector3Buffer(getVertexBuffer(),
            getVertexCount()));
      setNormalBuffer(BufferUtils.createVector3Buffer(getNormalBuffer(),
            getVertexCount()));
      getTextureCoords()
            .set(
                  0,
                  new TexCoords(BufferUtils
                        .createVector2Buffer(getVertexCount())));
      setTriangleQuantity(4 * (radialSamples + 1) * (axisSamples + 1));
      setIndexBuffer(BufferUtils.createIntBuffer(getIndexBuffer(),
            3 * getTriangleCount()));

      setGeometryData();
      setIndexData();
   }

   @Override
   public void write(JMEExporter e) throws IOException {
      super.write(e);
      OutputCapsule capsule = e.getCapsule(this);
      capsule.write(getAxisSamples(), "axisSamples", 0);
      capsule.write(getRadialSamples(), "radialSamples", 0);
      capsule.write(getOuterRadius(), "outerRadius", 0);
      capsule.write(getInnerRadius(), "innerRadius", 0);
      capsule.write(getHeight(), "height", 0);
   }
}



Thoughts
I wasn't really concerned about textures. I also found that if we don't need to apply textures to the top and bottom surfaces, we can get rid of the extra vertices added specially for them and just connect vertices from the inner and outer cylinders. This would reduce the number of vertices by 4xRadialSamples, but the number of triangles will still be the same.

I'm still very new to graphics in general. I'd be happy to hear your comments and feedback.

Do you know how to create a patch?

Yes. I created two separate project-rooted patches for src/com/jme/scene/shape/Disk.java and src/com/jme/scene/shape/Tube.java. I also included the test I used while making the changes.



Changes

Also for the tube, I added 8 more vertices to apply texture to the vertical edges, but I think the texture coordinates can be changed to make the texture more continuous around the inner and outer surfaces.



Patches

Disk.java.patch

Tube.java.patch



Test

AngleParamTest.java

Cool, great job! One question though, why do you need to add additional vertices just for texture coordinates? Couldn't you just index the ones that are already there that makeup the shape?



Thanks for the contrib

nymon said:

Cool, great job! One question though, why do you need to add additional vertices just for texture coordinates? Couldn't you just index the ones that are already there that makeup the shape?

Thanks for the contrib


Thanks nymon.

My current understanding of textures is that there is a single texture coordinate for each vertex. So, I thought if we reuse some vertices to close the shape, we can't add new texuture coordinates for them. Maybe that's wrong. Also, all I know about the index is that it tells the rendrer how to connect vertices to draw say, triangles. I don't know about the relation between index and texture.

As I said, I'm still new to graphics programming. I'd be glad if you could mention somewhere to get more information on textures. If it turned out that we don't need the extra vertices for the texture, I'll go ahead and remove the unnecessary ones and post my changes.

Thanks again.

The indicies simply relate to the vertices and the texture coordinates relate to the vertices.  So if there are no vertices no texture coordinates are needed (or even used)…



Also, maybe the angle should just be normalized between -2pi and 2pi,  so a rotation of 3pi would be 1pi; rather than 'wrapping' them around…

nymon said:

One question though, why do you need to add additional vertices just for texture coordinates? Couldn't you just index the ones that are already there that makeup the shape?


I found that we can avoid adding additional vertices, and still get texture on all edges, but it's not the same. In the screen shot below, the tube to the right didn't have additional vertices while the one to the left did. For the tube that had additional vertices, texture coordinates can be modified, but I didn't get into that. I also normalized the angle as basixs suggested.



Patches
Tube with no additional vertices (right)
Tube with additional vertices (left)

Tests
This test is more interactive and says more about the new tube.
TubeTest.java

Thanks for the feedback.

Looks pretty nice and the effort is very appreciated, but could you try one last thing?

Could you try to use the 'non-added' one (right) and create the 'disc' texture that you have in the first screen shots, on the 'ends'?  It should be fairly simple to do with cos and sin as your x/y texture values…



Also, (and this is not policy) but could you re-do the tube patch without changing the existing formatting?  The nice thing about viewing the changes with patches/diffs is that it limits all the things that have to be looked at; but when every line has changed due to re-formatting that bonus is lost. 

(this is a habit I have also had to break since I like to re-format constantly…)

If we don't add more textures, we're stuck with the texture coordinates for the inner and outer cylinders, because these are the most important or largest surfaces and it's obvious how the texture should be applied to them. Also, it's not clear how the texture should be applied to the other surfaces like the top and bottom disk-like surfaces and the rectangle-like vertical ones.



I did my best to honor your request using the available textures, I was able to apply the texture to both ends by simply flipping the inner cylinder texture vertically, so it meets the outer cylinder at opposite ends of the texture at the ends, and they can then interpolate over the whole texture in a radial way (first screen shot). This makes the texture continuous through the outer-top-inner-bottom-outer… surfaces, and I think this is nice. The other way to do that is to flip it horizontally, you'll be able to apply the whole texture to the vertical edges, but nothing for the top and bottom (second screen shot). In both cases, the side-effects of reusing texture coordinates is that some surfaces will have the texture flipped or mirrored which might not be desired.








Changes
This is a detailed list of changes:
1. Added the arcAngle paramter to the shape constructors.
2. Extended the cos/sin arrays by one element to avoid the mod in each single iteration.
3. Removed assignments from setters so it happens only in the updateGeometry method.
4. Added the new arcAngle field to the read/write methods.
5. Modified the setGeometryData method:
5.1. Create vertices according to the arcAngle parameter.
5.2. Remove extra vertices for top and bottom surfaces.
5.3. Flipped inner cylinder texture coordinates.
6. Modified the setIndexData method:
6.1. Changed top edge and bottom edge connections to reuse the vertices from outer and inner cylinders.
6.2. Added connections for the new vertical edges.
7. Changed updateGeometry method:
7.1. Normalize and assign the arcAngle parameter.
7.2. Reduce the number of vertices by 4 * radialSamples.
7.3. Increase the number of triangles by 4 * (axisSamples + 1) needed to draw the vertical edges.

Patches
I made sure the batch contains only the required changes, nothing is due to formatting.
Tube.java.patch (top)
Tube.java.patch (bottom)

Did anyone get a chance to check this out?



We had two options and I wanted to know which one you think is better.



Thanks.

You are probably gonna hate me, but would it be possible to eliminate the weird stretching on the top (top image).  Rather than just using the image edge for the inside tube edge, I think it would look a lot better if the center was just missing and the image is not stretched…  (just like you had in the first tube images)



(Also, for the image flipping you should be able to just swap the x coordinates for a horizontal flip and the y coordinates for a vertical flip)



Then I think that will be the one (the top one) :slight_smile:

Hey, that's pretty cool :slight_smile:

Hi there,



I'm sorry for the delay. I've been a little busy.


basixs said:

You are probably gonna hate me, but would it be possible to eliminate the weird stretching on the top (top image).  Rather than just using the image edge for the inside tube edge, I think it would look a lot better if the center was just missing and the image is not stretched...  (just like you had in the first tube images)


Not at all  :) I'm just trying to get it right.

I hope this is how it should look like:



Changes:
Tube.java.patch

Starnick said:

Hey, that's pretty cool :)


Thanks Starnick  :)

Thats sweet!



Looks really sharp now :slight_smile:



Unless anyone else has any suggestions I think that's the one…



(wanna make another patch?)

nice work, the patch is already there basixs

lol, so it is



(must have been sleepy still…)

I'm glad you liked it.



However, I still think that the previous version had some advantages. Aside from using less vertices, applying the texture continuously through all surfaces is maybe the right choice sometimes. One example is when the texture is not an image, say, some surface pattern which gives a certain look and feel, I think this is encountered frequently.  I know you didn't like how the image was streched around the top surface, but I think this is how it should act in that case. Having the top and bottom surfaces with a hole in the middle won't be pretty then, like in the image below. The texture I used for this is just two vertical blue strips on a white background.



(left: previous version, right: new agreed-upon version)



This is just a thought though. If nobody had something to add here for a couple of days, I'll consider it done.

Also, I wanted to know how people used to create "partial" tubes before in jME.

Thanks for your comments and feedback.

Hmm, how hard would it be to have a boolean flag and a setter? (and allow the user to choose)


Also, I wanted to know how people used to create "partial" tubes before in jME.

Most of the time people just model something to get what then need exactly, but its always nice to have some good built in objects :)

Just put both versions in and decide in the constructor which version to use.

I think doesn't need to be changeable once its created. (of course it wouldn't hurt)

Those patches have all disappeared from Pastebin - does anyone still have them kicking around somewhere?