Java glTF MeshBuilder
This project transforms simple 2D arrays into 3D meshes represented as glTF models. The meshes can be planar or wrapped about the x-axis or y-axis to generate cylindrical and spherical shapes that include:
- Planar: The points are connected to create a 2D terrain with elevations.
- Lathe: The edge of the y-axis is wrapped to create cylindrical shapes.
- Manifold: Both axes are wrapped to create solid spherical shapes.
The MeshBuilder class provides a fluent interface that saves effort because only the locations of vertices need to be specified. Normal vectors are calculated automatically to provide smooth and interpolated reflections from the surface. The resulting glTF model includes:
- Triangle indices: Indicates which points should render triangles
- Texture coordinates: Maps points on a texture to a mesh
- Normal Vectors: Used to interpolate reflections off surfaces so they appear smooth
- glTF buffers: Binary data stored in a glTF file that contains geometry
The builder will assume that points in the array should be connected in a square grid. Triangular geometries are not supported. You can optionally specify a PNG or JPEG texture and coordinates will automatically be interpolated to fit.
JUnit tests in
com.kinetica.mesh.demo provide examples for generating glTF models using various methods with and without textures. You can generate the models by running
maven test or by launching the JUnit tests from eclipse. Generated files will be placed in
./demo. You can see the results with an online glTF Viewer.
Below is a summary of the JUnit tests. These can be executed with
|TestShapeModels.testPlane()||Generate 3D graph addPlane().|
|TestShapeModels.testDiamond()||Generate a diamond shape addPlane().|
|TestShapeModels.testHelix()||Generate a textured helix with addLathe().|
|TestShapeModels.testTorus()||Generate a textured torus with addManifold().|
|TestCubeModel.testBox()||Generate a cube with textures on all sides.|
The surface uses the API
Meshbuilder.addPlane() to render a simple function. The function is calcuated for each grid point and a texture is added to the surface.
The diamond shape is generated from passing a 12x3 grid to
Meshbuilder.addLathe() which joins the grid around the y-axis to form a cylindrical shape. At each of the 3 points on the y-axis the API
Meshbuilder.addCircleVerticesXZ() is called to generate a 12-sided circle. Instead of providing a textue colors are specified for each of the circles that get interpolated.
The helix is generated by passing a 12x60 grid to
Meshbuilder.addLathe(). For each of the 60 steps along the y-axis the API
Meshbuilder.addCircleVerticesXZ() is called to generate a 12-sided circle. The radius of the circle is moved up the y-axis with a helix motion.
MeshBuilder.build() is passed a a texture which is fitted around the surface.
The torus is constructed with
Meshbuilder.addManifold() that will join the x-axis and y-axis. It is constructed from circles along the XZ horizontal plane that start on the outer radius and move around the edge to the inner radius. It results in a 12x48 size grid that is fitted with a texture and joined on both axis.
The cube gives an example of how to generate models using more primitive API's. Each vertex is created using
Meshbuilder.newVertex(). These are passed to
Meshbuilder.addSquare() to generate each of the 6 faces. Because it does not call one of the mesh generator routines It must manually add texture coordinates with
MeshVertex.setTexCoord(); however, normals are still generated automatically.
This section describes the basic process of using the MeshBuilder. The process involves creating a 2D array of vertex objects that represent points on the grid. See the JUnit test cases below for more details.
Create one or more a
MeshBuilderobjects. Each of these will result in a single glTF mesh. Name is a required parameter for the
MeshBuilderand will be referenced in the logs and the resulting file.
MeshBuilder.setTransform()to transform the size and position of the resulting geometry. These methods will cause points to be transformed as they are added and do not use any glTF transform API.
Create an empty 2D array of
MeshVertexobjects. Each vertex represents a point on a rectangular grid that will be curved in 3D space.
MeshVertexarray with values. Call
MeshBuilder.newVertex()to create the points. You can optionally assign a color to the vertex if the mesh will not be textured.
MeshBuilder.addManifold()to populate the mesh with geometry. At the lowest level this will add triangles to the mesh.
GltfWriter. Optionally call
GltfWriter.setAlphaMode()to indicate if the mesh should be viewable from both sides.
GltfWriter.addMaterial()to create the glTF material for the surface. For textures this should specify an image file.
Add each mesh to the file by calling
GltfWriter.writeGltf()to create the file. You can specify a
gltfextension to indicate the file format.
MeshBuilder class has the capability to automatically compute normals for any type of geometry without effort from the user. Normals are vectors that point perpendicular to the plane at each vertex and are necessary for the renderer to properly calculate shading interpolations for a surface. This interpolation gives the appearance of a smooth surface when the wireframe has limited detail.
Below is an exmaple of the of the same wireframe with and without vertex normals. Without the normals the renderer is lacking information about the surface and has no option except to simulate normals independently for each triangle.
Some apporaches to computing the normals involve manually calculating the derivative of the surface which requires considerable effort. The
MeshBuilder uses a relatively simple algorithm where normals for each vertex are calcuated from the average contribution of each triangle that includes it.
Above we can see an example of a minimal grid with 3x3 vertices, 2x2 square cells, and 8 triangles. The vertex in the center is shared by 6 triangles and its normal can be calculated by averaging the normals of each of its surrounding triangles. In this case the north/south and east/west contributions of the triangle normals will cancel and the vertex normal will point directly up.
The normals calculation requires that all triangles are added to the wireframe before the normals are calculated. The steps are as follows:
- Any time a shape is added with the MeshBuilder it will eventually call
GeometryBuilder.addTriangle()for every triangle to be added.
GeometryBuilder.addTriangle()will calculate the normal of the triangle and it to a list of normals in
MeshVertexfor each of the 3 vertices.
- Afer all triangles are added
GeometryBuilder.build()will average the normals for each
MeshVertexto calculate the normals.
The MeshBuilder leverages the JglTF library which can manipulate metadata but JglTF has no functionality to manipulate the glTF buffer which contains the geometry. The lower level functions of the MeshBuilder fill this gap by allowing for geometry primitives to be set and serialized the buffer.
A glTF file consists of 3 basic types of data:
- JSON metadata: You can see this if you open the glTF file with a text editor.
- Binary buffer: This is base64 encoded data containing lists of geometry data.
- Images: This is base64 encoded containing JPG images.
The metadata is a hierarchy of objects which at the lowest level provides access to the data buffer. For a detailed specification see the glTF Specification.
Buffer: At this lowest level the data is a raw sequence of bytes.
BufferView: This object references a section of the of the buffer.
Accessor: Abstracts raw bytes as primitives vectors or scalars.
Material: Indicates surface features of Mesh like texture and reflectivity.
Mesh: Pulls together Accessors and a Material to define a surface.
Node: Groups a set of meshes into an object.
Scene: Groups a set of Nodes
Buffer serialization starts in
GeometryBuilder.buildBuffers(). It constructs a set of serializers subclassed from
mesh.buffer.BaseBuffer to handle each type of primitive:
Vertices: A list of 3D points.
TexCoords: A list of 2D points that map a texture to a position on a Mesh.
VertexColors: A list of RGB colors for vertices.
Normals: A list of 3D vectors that are orthogonal to the surface at each vertex.
Tangents: A list of 4D vectors that are tangent to the surface at each vertex.
TriangleIndices: Indices that reference vertices in groups of 3 for drawing triangles.
Tangents serializer is currently a partially implemented feature used for bump mapping.
TriangleIndices each of the serializers should have N values where N is the number of vertices. For example if there are N vertices then there should be N normal vectors and N texture coordinates for the vertices.
The serializers are populated with data from the MeshVertex list. Next their
BaseBuffer.build() is called which will serialize contents to the buffer and add necessary JSON metadata.
High level classes that generate 3D models:
|mesh.GltfWriter||Generate glTF binary and JSON encoded files|
|mesh.GeometryBulder||Generate meshes based on shape primitives.|
|mesh.MeshBuilder||Generate 3D surfaces from an array of MeshVertex objects.|
|mesh.MeshVertex||Contains all information to describe a point in a mesh.|
Classes used to generate 3D primitives:
|mesh.buffer.BaseBuffer||Base class for primitive buffers|
|mesh.buffer.Normals||Normal vectors used for light interpolation|
|mesh.buffer.TexCoords||Coordinates used to map textures to vertices|
|mesh.buffer.TriangleIndices||Map vertices to triangles|
|mesh.buffer.VertexColors||Map colors to vertices|
|mesh.buffer.Vertices||Indicates a point in 3D space|
The following resources are useful for understanding the glTF specification.
Chad Juliano: firstname.lastname@example.org
MIT License: http://spdx.org/licenses/MIT