Constructive solid geometry (CSG) (formerly called computational binary solid geometry) is a technique used in 3D solid modeling. Constructive solid geometry allows a modeler to create a complex surface or object by using Boolean operators to combine objects. Often CSG presents a model or surface that appears visually complex, but is actually little more than cleverly combined or decombined objects
It is said that an object is constructed from primitives by means of allowable operations, which are typically Boolean operations on sets: union, intersection and difference.
- Union - the merger (addition) of two objects into one
- Intersection - the portion common to both objects
- Difference - what is left when you remove one object from another
A Few Personal Notes on the History of this Code (Q1 2015)
After retiring from the professional life of a software engineer for over four decades, I have the opportunity to indulge in a few whims and can now investigate things like 3D graphics. Having experimented with Java since its initial release in 1995, and having been developing enterprise-level, commercial java applications since before 2000, jMonkey struck me as perfect place to begin playing around.
As an engineer, not a graphics designer, the idea of hand-tooling meshes and textures via various UI oriented drawing tools has no appeal. I am a build-it-up-programmatically kind of person. So when I went looking for the concept of blending shapes together, I came across CSG and a sample implementation in Java for jMonkey.
As far as I can tell, Evan Wallace put together a Javascript library for CSG support within
browsers that support WebGL. The underlying processing algorithm is BSP - Binary Space Partitioning
@see http://evanw.github.io/csg.js (it is really quite impressive).
This was converted into jMonkey compatible Java by fabsterpal, and posted to a
Github repository
by andychase. Everything seems to have been properly posted and annotated for fully open
source.
While working with the java code, I tripped over some bugs and encountered many spots
where I wished for more extensive comments. My personal learning style is to work from an
operational example. From there, I can make small, incremental changes to help me understand
the larger picture. To that end, I reimplemented the Java code, following my own
conventions and structures.
But the logic and algorithms are all based directly on what I found
in the original Javascript/Java code.
Unfortunately, personal experience with this code, and further research on the web, lead me to believe there is an inherent problem with BSP. Due to the precision limitations on any real computer, artifacts are introduced into more complex shapes. Therefore, I have implemented a different algorithm based on the work of Danilo Balby, Silva Castanheira, who gratiously posted his work into the public domain.
In other words, I am frolicking in my sandbox... But as a firm believer in open source, I am making my
experiments available to all. If there is something here of value to you, feel free to use it.
I will be attempting to keep the posted code fully operational -- but for now, I am making no pledges
about backward compatibility.
If you find a problem, post a ticket
via SourceForge.
Code Structure
I am attempting to retain the CSG code as an independent plug-in with no changes needed within the jme3 core. You will find the interesting stuff in the package "net.wcomohundro.jme3.csg". Some habits die hard (well, they really do not die at all), so I work within the Eclipse IDE, using jMonkey source as provided by the latest stable jme3 SDK. As of 30May2018, that is version 3.2.1
All that being said, I do have a set of core jme3 changes posted in the CSG SVN repository. (oh, come on, you just knew that was coming) I have found the Savable support via XML to be an excellent way to manage my test cases. My core code changes are mostly related to providing reasonable (ie, non NullPointerException) defaults for elements missing from the XML. And I have created a simple XMLLoader plugin for the AssetManager that allows me to load Assets from the XML files. This allows me to create CSG test cases very simply by hand editing XML files. By providing reasonable defaults, I can prune my XML files to a bare minimum.
The core CSG classes are defined in the WCOmoCSG321... jar. This jar can be included within any jMonkey IDE project, which can then utilize all the core functionality. However, if you want to use the XML loader functions, then you will need to include the WCOmoCSGjme321... jar and position it before any of the standard jme3 jars. The XML import functions will then be available. Test files, and their associated assets, are available in the WCOmoCSGtest321... jar. Any and all source can be downloaded from SourceForge.
Features
- Primitive shapes based on any arbitrary Mesh
- Composite geometry based on boolean blending of the shapes
- Materials assigned to the shape apply to the surface generated by that shape in the final geometry. In other words, the result can have multiple Materials.
- CSG specific primitive shapes support faces with different texture scaling and materials applying to the different surfaces (ie, front, back, top, bottom, ...)
- Local lights assigned to the shape apply to the surface generated by that shape in the final geometry. In other words, the result can have Node-scoped lights that apply only to selected subsurfaces.
Basic Elements
To use CSG, you will be working with shapes and geometries.
- CSGShape
- The basic CSG primitive based on any arbitrary Mesh.
- CSGGeometry
- The boolean blending point for the various CSGShapes which results in a final shape to which a single Material is applied.
- CSGGeonode
- The boolean blending point for the various CSGShapes, each with it own optional material, which results in a final shape. The materials and local lights assigned to the primitives carry through onto the surface that they produce in final result. Primitives without an explicit material use the material assigned to the CSGGeonode as a whole.
My original design intent was to define CSGGeometry for the blending point, with an ability to support multiple materials. As I learned more about jMonkey, I found that Geometry can only readily support a single Mesh. To provide for multiple materials that bleed through from the primitives, you really need to leverage a Node with Geometry children. Therefore, CSGGeonode was created. Multiple meshes are produced corresponding to the different materials. CSGGeometry is no longer strictly required, since CSGGeonode provides all the same services. But CSGGeometry does provide optimized processing for those objects that have a single material.
CSG Primitive Shapes
CSGShape can be built from any mesh. No further primitives other than those provided by the core jMonkey services are required, and my original design intent was to not create any CSG specific primitives. But during the course of my testing, I tripped over issues around textures for Box, Cylinder, and Sphere. Therefore, I have created some CSG primitives.
The CSG primitives try to take a unified approach to their definition. This includes the idea of shapes produced by multiple slices taken along the z-axis. And retaining the concept of a face (front, back, top, bottom, ...), where the texture on each surface may be independently scaled and a different Material applied.
- For a CSGBox, this means that a single repeating texture can be appropriately scaled to remain undistorted for any elongated edge.
- For a CSGCylinder or CSGPipe, the closed end caps are not radially distorted, but simply represent a circular cutout of the texture, which can be independently scaled.
- For a CSGSphere, there is currently no special definition of a surface.
Other enhancements include:
- The radial distortion of the texture at the Sphere's poles has been eliminated.
- You can choose to run the curved surface texture of a Cylinder along the height, rather than along the circumference.
- An x/y scale can be applied to every individual slice along the z-axis.
- A rotation can be applied to every individual slice around the z-axis. This results in a twisted final shape.
- A simple Spline generator that can easily produce a torus or a helix.
- LOD processing is supported by reducing the number of slices by a given percentage.
A future enhancement is to reduce the number of radial points as well. - TangentBinormal generation can be triggered by a setting in the XML Savable file.
Licensed - BSD like jMonkey
New BSD (3-clause) License. In other words, you do whatever makes you happy!
Download/Access the Code
jMonkeyCSG is managed via a SourceForge project which you can reference here. The .jar and .zip files can be downloaded from here. All source is available via the SourceForge SVN repository.
The jMonkey IDE
As I stated earlier, I work in Eclipse, referencing the jme3 source as provided by a stable release of the jme3 SDK. I am not too familiar with the jMonkey IDE, but have gotten CSG to work simply by including the core CSG jar file in the Libraries section of a jMonkey IDE project.
Getting Started
The simplest example is to add some shapes into a geometry, regenerate that geometry and apply a Material, and add the geometry into your scene.
// Blend shapes into a geometry
CSGGeometry aGeometry = new CSGGeometry();
aGeometry.setMaterial( new Material( assetManager, "Common/MatDefs/Misc/ShowNormals.j3md" ) );
// Start with a sphere
CSGShape aSphere = new CSGShape( "Sphere1", new Sphere( 32, 32, 1.3f ) );
aGeometry.addShape( aSphere );
// Subtract out a cube
CSGShape aCube = new CSGShape( "Box", new Box(1,1,1) );
aGeometry.subtractShape( aCube );
// Produce the final shape
aGeometry.regenerate();
// Now add aGeometry to your scene
If you want to apply materials to the primitives, and have those materials appear on the appropriate surfaces in the final result, do something like the following.
// Blend shapes into a geonode with an overall material
CSGGeonode aGeometry = new CSGGeonode();
aGeometry.setMaterial( new Material( assetManager, "Common/MatDefs/Misc/ShowNormals.j3md" ) );
// Start with a cube
CSGShape aCube = new CSGShape( "Box", new Box(1,1,1) );
aGeometry.addShape( aCube );
// Subtract out a colored cylinder
CSGShape aCylinder = new CSGShape( "Cylinder", new Cylinder( 32, 32, 1.1f, pLength, true ) );
Material mat1 = new Material( assetManager, "Common/MatDefs/Misc/Unshaded.j3md" );
mat1.setColor( "Color", ColorRGBA.Yellow );
aCylinder.setMaterial( mat1 );
aGeometry.subtractShape( aCylinder );
// Produce the final shape
aGeometry.regenerate();
// Now add aGeometry to your scene
Test Cases
Various test cases are included in the SVN repository, all based on the Savable XML format. For example, a corridor can be created by subtracting a cylinder from an elongated box, and then cutting out a doorway. As you can see, it is quite easy to build up complex examples very quickly by editing the XML file.
<net.wcomohundro.jme3.csg.CSGLinkNode fname='CSGSample'>
<lights class='com.jme3.light.LightList'>
<lights>
<com.jme3.light.AmbientLight name='WhiteLight' enabled='true'>
<color class='com.jme3.math.ColorRGBA' r='1' g='1' b='1' a='1'/>
</com.jme3.light.AmbientLight>
</lights>
</lights>
<children>
<net.wcomohundro.jme3.csg.CSGGeonode name='ACorridor'
materialName='Textures/Rock/Rock1Rpt.xml'>
<shapes>
<net.wcomohundro.jme3.csg.CSGShape name='Exterior'>
<mesh class='net.wcomohundro.jme3.csg.shape.CSGBox'
xExtent='1.0' yExtent='1.0' zExtent='1.0'>
<faceProperties>
<net.wcomohundro.jme3.csg.shape.CSGFaceProperties face='FRONT_BACK' scaleX='1' scaleY='1.0'/>
<net.wcomohundro.jme3.csg.shape.CSGFaceProperties face='LEFT_RIGHT' scaleX='10' scaleY='1.0'/>
<net.wcomohundro.jme3.csg.shape.CSGFaceProperties face='TOP_BOTTOM' scaleX='1' scaleY='10.0'/>
</faceProperties>
</mesh>
<transform class='com.jme3.math.Transform'>
<scale class='com.jme3.math.Vector3f' x='1.0' y='1.0' z='10.0'/>
</transform>
</net.wcomohundro.jme3.csg.CSGShape>
<net.wcomohundro.jme3.csg.CSGShape name='Interior' operator='DIFFERENCE'
materialName='Textures/BrickWall/BrickWallRpt.xml'>
<mesh class='net.wcomohundro.jme3.csg.shape.CSGCylinder'
closed='true' height='1.0' radius='0.5' textureMode='FLAT_LINEAR'>
<faceProperties>
<net.wcomohundro.jme3.csg.shape.CSGFaceProperties face='SIDES' scaleX='9.95' scaleY='3.0'/>
</faceProperties>
</mesh>
<transform class='com.jme3.math.Transform'>
<scale class='com.jme3.math.Vector3f' x='1.95' y='1.95' z='19.9'/>
</transform>
</net.wcomohundro.jme3.csg.CSGShape>
<net.wcomohundro.jme3.csg.CSGShape name='Doorway' operator='DIFFERENCE'>
<mesh class='net.wcomohundro.jme3.csg.shape.CSGBox'
xExtent='0.9' yExtent='0.5' zExtent='0.9'/>
<transform class='com.jme3.math.Transform'>
<translation class='com.jme3.math.Vector3f' x='0' y='0.5' z='10'/>
</transform>
</net.wcomohundro.jme3.csg.CSGShape>
</shapes>
</net.wcomohundro.jme3.csg.CSGGeonode>
</children>
</net.wcomohundro.jme3.csg.CSGLinkNode>