Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
scroll top