2

I have a Java application that needs to generate mathematically-defined 3D shapes for a voxel world (Minecraft specifically, but that's not important to the discussion). These include sphere, ovoid, ellipsoid, cone, capsule, and others, and there are two distinct applications that I have for them. Currently I have a single Shape class which Sphere, Capsule, etc extend. Each shape has its own set of parameters like Centre, Radius, Height, etc.

The first way I'm using these shapes is to generate solid objects by iterating over the voxel grid and determining which voxels lie inside the shape. This gives me a boolean Contains(Vector v) method for each unique shape, and Fill() which is generic to all solid shapes.

The second way I'm using them is to generate a field of randomised points that do not lie on the grid, which will need a method like Vector GetRandomPoint() for each shape, and GenerateRandomPoints() which is generic to all point generators.

Neither of these types of shape can reuse each others' code. Solid shapes are to determine which voxels lie inside them, and point generators create floating point vectors. The random points can't be generated naively then filtered for whether they lie inside the shape (ie, using the Contains() function) for technical reasons that would take too long to explain here.

To add yet another wrinkle, there is a noise component which both classes need as well, and this component can be generic to all shapes and applications.

The way I'm considering doing this is to have two base classes SolidShape and PointGenerator, then have interfaces for each shape. So for example the two kinds of sphere would be:

public class SolidSphere extends SolidShape implements Sphere {...}

public class SpherePointGenerator extends PointGenerator implements Sphere {...}

This way the generic methods would be reused between shapes, and the dimension parameters like Centre and Radius would be reused between the two applications.

I'm still not sure how to make the noise component generic between all of them, though, although that may be solvable by simply making a Noise class and giving each shape a Noise member.

However, when I attempt to do this, it turns out interfaces aren't used for declaring member variables, and they don't let you do this. Is there a way to do this, or should I just replicate the member variables for each shape? I really don't want to have duplicates of the same variables hanging around because that fast becomes a maintenance nightmare.

12
  • 1
    Not sure why you need a specific kind of SphereCloud implementing Sphere, this abstraction does not look very natural to me. A point cloud is something I would expect to be a container of points, without any specific information of what kind of specific shape this originally was.
    – Doc Brown
    Mar 9, 2021 at 7:30
  • @DocBrown I want to generate point clouds with specific overall shapes, so the points all lie inside a given shape. And to anticipate an obvious solution, it would be easy to simply generate random points then check if they fall inside the shape, but this is not suitable because I also need a certain bias in the distribution that this wouldn't satisfy. That means I need to generate the points with the correct distribution the first time, which involves a significant amount of maths but we're getting into implementation details at this point. Mar 9, 2021 at 7:37
  • 1
    @Excrubulent: I would implement a SpherePointGenerator with a Sphere as constructor parameter, not deriving from Sphere. If your SpherePointGenerator requires attributes like Radius and Center, just delegate the getters to the Radius and Center of the referenced Sphere.
    – Doc Brown
    Mar 9, 2021 at 14:28
  • 1
    @DocBrown So for instance you'd have a SolidSphere, A SpherePointGenerator, and both of those classes would have a SphereDimensions member that contains all the common variables. That's... really simple actually. That seems really obvious now that you've said it, it solves the issue completely, and it requires very little refactoring. Mar 9, 2021 at 23:09
  • 1
    @Excrubulent: ok, I made an answer from this.
    – Doc Brown
    Mar 10, 2021 at 6:38

2 Answers 2

1

From the discussion in the comments, it seems a simple, but sufficient solution here could be the following approach:

  • a class SphereDimensions, which holds just center and radius.

  • classes SolidSphere and SpherePointGenerator, which take a SphereDimensions object as constructor parameter and stores the reference internally

  • SpherePointGenerator and SolidSphere providing a method getSphereDimensions(), returning the stored reference.

The principle behind this is called "prefer composition over inheritance", but that does not mean one must not use inheritance here: SolidSphere can still extend SolidShape, and SpherePointGenerator can still extend PointGenerator, this is orthogonal to the former suggestion.

I would design SphereDimensions to be immutable, since references to the same object will be reused in several places. So taking measures against unexpected side-effects can become crucial.

For the ease-of-use, all three classes might derive from a common interface Sphere (with public methods getRadius and getCenter, and no setters!), but that is merely "syntactic sugar", so one can replace

   mySolidSphere->getSphereDimensions()->getRadius()

by a shorter call

  mySolidSphere->getRadius()

or change methods which originally take a SphereDimensions object as input into ones which take a Sphere as input. This way, they can directly process a mySolidSphere object, instead of requiring mySolidSphere->getSphereDimensions().

The latter has also a name, it has the structure of the classic Proxy design pattern.

1
  • 1
    I've implemented this now, the overall structure is much easier to understand and some of the geometry-specific calculations have now been offloaded to the dimensions classes so it's a lot cleaner as well. I'll try to remember to prefer composition over inheritance in the future. Mar 12, 2021 at 13:58
0

Clarity

The shapes are all Euclidean primitive shapes.

There are three interpretations though:

  • Geometric interpretation
  • Voxel interpretation
  • Filtered Point cloud interpretation

Geometric interpretation is as defined by Euclid, with a Sphere being defined by a point and Radius.

Voxel Interpretation is determined by identifying how much of the voxel is within the Geometric solid, and applying some set of rules to determine how it is filled.

Filtered point cloud is determined by applying a set of rules to the geometric surface of the Euclidean solid, to generate a sub-sample of surface points.


Data-Structures and Interpreters

Step away from thinking of each shape as a self-contained object. Think of the shapes as a description, quiet possible as one part of a larger scene description. This description is the Data-Structure.

What you want though are two different data-structures:

  • A Voxel Set
  • A Floating-Point, Point Cloud obeying certain spacing rules

To translate the former shape model into the later voxel, or point cloud models apply an interpreter. It knows enough to apply the visitor pattern, or analyse the shape model via a query interface. From this the interpreter is responsible for constructing the voxel model or the point cloud using the implementation the suits it best.

Shared functionality can be pulled out into collaborating classes capable of interpreting some aspect of the shape model to answer some question.

For Example a voxel is a volume, a point is an infinitely small volume. You could create a collaborator that can interpret shapes in the model to determine if a volume is contained by the shape.


Domain Object

Specify the Shape interface

interface Shape
{
   bool contains(Point);
   bool contains(Voxel);

   Enumerable<Point> Points();
   Enumerable<Voxels> Voxels();

   //might I also recommend this to coerce a given point to the closest point on the shapes surface using some definition of closest.
   Point SnapToSurface(Point);

   //To return a box that this shape just fits in. You could also do spheres.
   AxisAlignedBoundingBox Bounds();
}

And define each shape to implement these operations efficiently. Afterall this is what it means to be a Shape.


Decorators

Old Answer, interpret this as refining the behaviour of the voxelisation, or point cloud.

A Shape is nothing more than a list of points that lie within it.

interface Shape
{
    Enumerable Points { get; }

    bool Contains(Point);
}

Implement it for any given shape you care for. Provide efficient of inefficient implementations for testing membership, or generating a set of members. But it always says if its in the shape or not.

A Point Cloud as you put it isn't anything special. Its a filter for selecting some subset of those points. So it implements the same interface, and somehow selects a subset of those points to be members.

class EveryOtherPoint implements Shape
{
   private Shape _shape;

   public EveryOtherPoint(Shape shape) { _shape = shape; }

   public Enumerable Points { get { return SubSample(_shape); } }

   private Enumerable SubSample(Enumerable points)
   {
       //algorithm for filtering.
   }

   public bool Contains(Point p) { return Points.Contains(p); }
}

Need a different Shape, just implement it with the variables that make sense to it. Provide the testing and generating members to go with it.

Need a different distribution? Apply a different decorating filter, or even overlay them.

10
  • Unfortunately a point cloud is not merely a subset of the voxels that make up a solid shape, they are mathematically distinct concepts. The point cloud is actually a series of floating point vectors that do not map 1:1 with the voxels, whereas the voxels are represented by integer vectors. Also I need a specific distribution of randomised points that can't be satisfied by creating an even distribution then filtering out points. The premise of my question is that these two applications don't really share functions. Mar 9, 2021 at 8:15
  • Also a solid shape is not merely a list of points, it is defined mathematically. Selecting the correct voxels to represent this shape is the problem being solved. Your code structure appears to assume that I already have a preset of the points I need and I simply need to select from them, but each shape is going to be slightly different. Because of the nature of the code that generates these shapes, they don't lie cleanly on individual voxels. Their orientation, size and position are all floating point, so no two shapes are likely to be exactly the same. Mar 9, 2021 at 8:19
  • @Excrubulent Perhaps you could clarify your question further then. You lead me to believe that all points were voxel points.
    – Kain0_0
    Mar 9, 2021 at 8:23
  • 1
    @Excrubulent By creating another class containing functions that take a shape as input, or the shape was provided as input to the constructor. Pretty sure this would have been covered in the first few chapters of a programming manual. But if that is your real question, then it is way of from what you asked.
    – Kain0_0
    Mar 9, 2021 at 10:54
  • 1
    @Excrubulent Perhaps but neither are you paying me to do it. Investigate the Interpreter pattern. Investigate refactoring particularly Sprout and Split. Investigate CQRS. These are all tangentially related along the theme of separating concerns perhaps you'll find your answer there.
    – Kain0_0
    Mar 9, 2021 at 11:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.