/*
* __ .__ .__ ._____.
* _/ |_ _______ __|__| ____ | | |__\_ |__ ______
* \ __\/ _ \ \/ / |/ ___\| | | || __ \ / ___/
* | | ( <_> > <| \ \___| |_| || \_\ \\___ \
* |__| \____/__/\_ \__|\___ >____/__||___ /____ >
* \/ \/ \/ \/
*
* Copyright (c) 2006-2011 Karsten Schmidt
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* http://creativecommons.org/licenses/LGPL/2.1/
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
package toxi.geom.mesh;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import toxi.geom.AABB;
import toxi.geom.Intersector3D;
import toxi.geom.IsectData3D;
import toxi.geom.Matrix4x4;
import toxi.geom.Quaternion;
import toxi.geom.Ray3D;
import toxi.geom.ReadonlyVec3D;
import toxi.geom.Sphere;
import toxi.geom.Triangle3D;
import toxi.geom.TriangleIntersector;
import toxi.geom.Vec2D;
import toxi.geom.Vec3D;
import toxi.math.MathUtils;
/**
* An extensible class to dynamically build, manipulate & export triangle
* meshes. Meshes are built face by face. This implementation automatically
* re-uses existing vertices and can generate smooth vertex normals. Vertice and
* face lists are directly accessible for speed & convenience.
*/
public class TriangleMesh implements Mesh3D, Intersector3D {
/**
* Default size for vertex list
*/
public static final int DEFAULT_NUM_VERTICES = 1000;
/**
* Default size for face list
*/
public static final int DEFAULT_NUM_FACES = 3000;
/**
* Default stride setting used for serializing mesh properties into arrays.
*/
public static final int DEFAULT_STRIDE = 4;
protected static final Logger logger = Logger.getLogger(TriangleMesh.class
.getName());
/**
* Mesh name
*/
public String name;
/**
* Vertex buffer & lookup index when adding new faces
*/
public LinkedHashMap<Vec3D, Vertex> vertices;
/**
* Face list
*/
public ArrayList<Face> faces;
protected AABB bounds;
protected Vec3D centroid = new Vec3D();
protected int numVertices;
protected int numFaces;
protected Matrix4x4 matrix = new Matrix4x4();
protected TriangleIntersector intersector = new TriangleIntersector();
protected int uniqueVertexID;
public TriangleMesh() {
this("untitled");
}
/**
* Creates a new mesh instance with initial default buffer sizes.
*
* @param name
* mesh name
*/
public TriangleMesh(String name) {
this(name, DEFAULT_NUM_VERTICES, DEFAULT_NUM_FACES);
}
/**
* Creates a new mesh instance with the given initial buffer sizes. These
* numbers are no limits and the mesh can be smaller or grow later on.
* They're only used to initialise the underlying collections.
*
* @param name
* mesh name
* @param numV
* initial vertex buffer size
* @param numF
* initial face list size
*/
public TriangleMesh(String name, int numV, int numF) {
init(name, numV, numF);
}
public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c) {
return addFace(a, b, c, null, null, null, null);
}
public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c, Vec2D uvA,
Vec2D uvB, Vec2D uvC) {
return addFace(a, b, c, null, uvA, uvB, uvC);
}
public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c, Vec3D n) {
return addFace(a, b, c, n, null, null, null);
}
public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c, Vec3D n, Vec2D uvA,
Vec2D uvB, Vec2D uvC) {
Vertex va = checkVertex(a);
Vertex vb = checkVertex(b);
Vertex vc = checkVertex(c);
if (va.id == vb.id || va.id == vc.id || vb.id == vc.id) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("ignorning invalid face: " + a + "," + b + "," + c);
}
} else {
if (n != null) {
Vec3D nc = va.sub(vc).crossSelf(va.sub(vb));
if (n.dot(nc) < 0) {
Vertex t = va;
va = vb;
vb = t;
}
}
Face f = new Face(va, vb, vc, uvA, uvB, uvC);
faces.add(f);
numFaces++;
}
return this;
}
/**
* Adds all faces from the given mesh to this one.
*
* @param m
* source mesh instance
*/
public TriangleMesh addMesh(Mesh3D m) {
for (Face f : m.getFaces()) {
addFace(f.a, f.b, f.c, f.uvA, f.uvB, f.uvC);
}
return this;
}
public AABB center(ReadonlyVec3D origin) {
computeCentroid();
Vec3D delta = origin != null ? origin.sub(centroid) : centroid
.getInverted();
for (Vertex v : vertices.values()) {
v.addSelf(delta);
}
getBoundingBox();
return bounds;
}
private final Vertex checkVertex(Vec3D v) {
Vertex vertex = vertices.get(v);
if (vertex == null) {
vertex = createVertex(v, uniqueVertexID++);
vertices.put(vertex, vertex);
numVertices++;
}
return vertex;
}
/**
* Clears all counters, and vertex & face buffers.
*/
public TriangleMesh clear() {
vertices.clear();
faces.clear();
bounds = null;
numVertices = 0;
numFaces = 0;
uniqueVertexID = 0;
return this;
}
public Vec3D computeCentroid() {
centroid.clear();
for (Vec3D v : vertices.values()) {
centroid.addSelf(v);
}
return centroid.scaleSelf(1f / numVertices).copy();
}
/**
* Re-calculates all face normals.
*/
public TriangleMesh computeFaceNormals() {
for (Face f : faces) {
f.computeNormal();
}
return this;
}
/**
* Computes the smooth vertex normals for the entire mesh.
*/
public TriangleMesh computeVertexNormals() {
for (Vertex v : vertices.values()) {
v.clearNormal();
}
for (Face f : faces) {
f.a.addFaceNormal(f.normal);
f.b.addFaceNormal(f.normal);
f.c.addFaceNormal(f.normal);
}
for (Vertex v : vertices.values()) {
v.computeNormal();
}
return this;
}
/**
* Creates a deep clone of the mesh. The new mesh name will have "-copy" as
* suffix.
*
* @return new mesh instance
*/
public TriangleMesh copy() {
TriangleMesh m = new TriangleMesh(name + "-copy", numVertices, numFaces);
for (Face f : faces) {
m.addFace(f.a, f.b, f.c, f.normal, f.uvA, f.uvB, f.uvC);
}
return m;
}
protected Vertex createVertex(Vec3D v, int id) {
return new Vertex(v, id);
}
public TriangleMesh faceOutwards() {
computeCentroid();
for (Face f : faces) {
Vec3D n = f.getCentroid().sub(centroid);
float dot = n.dot(f.normal);
if (dot < 0) {
f.flipVertexOrder();
}
}
return this;
}
public TriangleMesh flipVertexOrder() {
for (Face f : faces) {
Vertex t = f.a;
f.a = f.b;
f.b = t;
Vec2D tuv = f.uvA;
f.uvA = f.uvB;
f.uvB = tuv;
f.normal.invert();
}
return this;
}
public TriangleMesh flipYAxis() {
transform(new Matrix4x4().scaleSelf(1, -1, 1));
flipVertexOrder();
return this;
}
public AABB getBoundingBox() {
final Vec3D minBounds = Vec3D.MAX_VALUE.copy();
final Vec3D maxBounds = Vec3D.NEG_MAX_VALUE.copy();
for (Vertex v : vertices.values()) {
minBounds.minSelf(v);
maxBounds.maxSelf(v);
}
bounds = AABB.fromMinMax(minBounds, maxBounds);
return bounds;
}
public Sphere getBoundingSphere() {
float radius = 0;
computeCentroid();
for (Vertex v : vertices.values()) {
radius = MathUtils.max(radius, v.distanceToSquared(centroid));
}
return new Sphere(centroid, (float) Math.sqrt(radius));
}
public Vertex getClosestVertexToPoint(ReadonlyVec3D p) {
Vertex closest = null;
float minDist = Float.MAX_VALUE;
for (Vertex v : vertices.values()) {
float d = v.distanceToSquared(p);
if (d < minDist) {
closest = v;
minDist = d;
}
}
return closest;
}
/**
* Creates an array of unravelled normal coordinates. For each vertex the
* normal vector of its parent face is used. This is a convienence
* invocation of {@link #getFaceNormalsAsArray(float[], int, int)} with a
* default stride = 4.
*
* @return array of xyz normal coords
*/
public float[] getFaceNormalsAsArray() {
return getFaceNormalsAsArray(null, 0, DEFAULT_STRIDE);
}
/**
* Creates an array of unravelled normal coordinates. For each vertex the
* normal vector of its parent face is used. This method can be used to
* translate the internal mesh data structure into a format suitable for
* OpenGL Vertex Buffer Objects (by choosing stride=4). For more detail,
* please see {@link #getMeshAsVertexArray(float[], int, int)}
*
* @see #getMeshAsVertexArray(float[], int, int)
*
* @param normals
* existing float array or null to automatically create one
* @param offset
* start index in array to place normals
* @param stride
* stride/alignment setting for individual coordinates (min value
* = 3)
* @return array of xyz normal coords
*/
public float[] getFaceNormalsAsArray(float[] normals, int offset, int stride) {
stride = MathUtils.max(stride, 3);
if (normals == null) {
normals = new float[faces.size() * 3 * stride];
}
int i = offset;
for (Face f : faces) {
normals[i] = f.normal.x;
normals[i + 1] = f.normal.y;
normals[i + 2] = f.normal.z;
i += stride;
normals[i] = f.normal.x;
normals[i + 1] = f.normal.y;
normals[i + 2] = f.normal.z;
i += stride;
normals[i] = f.normal.x;
normals[i + 1] = f.normal.y;
normals[i + 2] = f.normal.z;
i += stride;
}
return normals;
}
public List<Face> getFaces() {
return faces;
}
/**
* Builds an array of vertex indices of all faces. Each vertex ID
* corresponds to its position in the {@link #vertices} HashMap. The
* resulting array will be 3 times the face count.
*
* @return array of vertex indices
*/
public int[] getFacesAsArray() {
int[] faceList = new int[faces.size() * 3];
int i = 0;
for (Face f : faces) {
faceList[i++] = f.a.id;
faceList[i++] = f.b.id;
faceList[i++] = f.c.id;
}
return faceList;
}
public IsectData3D getIntersectionData() {
return intersector.getIntersectionData();
}
/**
* Creates an array of unravelled vertex coordinates for all faces using a
* stride setting of 4, resulting in a serialized version of all mesh vertex
* coordinates suitable for VBOs.
*
* @see #getMeshAsVertexArray(float[], int, int)
* @return float array of vertex coordinates
*/
public float[] getMeshAsVertexArray() {
return getMeshAsVertexArray(null, 0, DEFAULT_STRIDE);
}
/**
* Creates an array of unravelled vertex coordinates for all faces. This
* method can be used to translate the internal mesh data structure into a
* format suitable for OpenGL Vertex Buffer Objects (by choosing stride=4).
* The order of the array will be as follows:
*
* <ul>
* <li>Face 1:
* <ul>
* <li>Vertex #1
* <ul>
* <li>x</li>
* <li>y</li>
* <li>z</li>
* <li>[optional empty indices to match stride setting]</li>
* </ul>
* </li>
* <li>Vertex #2
* <ul>
* <li>x</li>
* <li>y</li>
* <li>z</li>
* <li>[optional empty indices to match stride setting]</li>
* </ul>
* </li>
* <li>Vertex #3
* <ul>
* <li>x</li>
* <li>y</li>
* <li>z</li>
* <li>[optional empty indices to match stride setting]</li>
* </ul>
* </li>
* </ul>
* <li>Face 2:
* <ul>
* <li>Vertex #1</li>
* <li>...etc.</li>
* </ul>
* </ul>
*
* @param verts
* an existing target array or null to automatically create one
* @param offset
* start index in arrtay to place vertices
* @param stride
* stride/alignment setting for individual coordinates
* @return array of xyz vertex coords
*/
public float[] getMeshAsVertexArray(float[] verts, int offset, int stride) {
stride = MathUtils.max(stride, 3);
if (verts == null) {
verts = new float[faces.size() * 3 * stride];
}
int i = offset;
for (Face f : faces) {
verts[i] = f.a.x;
verts[i + 1] = f.a.y;
verts[i + 2] = f.a.z;
i += stride;
verts[i] = f.b.x;
verts[i + 1] = f.b.y;
verts[i + 2] = f.b.z;
i += stride;
verts[i] = f.c.x;
verts[i + 1] = f.c.y;
verts[i + 2] = f.c.z;
i += stride;
}
return verts;
}
public float[] getNormalsForUniqueVerticesAsArray() {
float[] normals = new float[numVertices * 3];
int i = 0;
for (Vertex v : vertices.values()) {
normals[i++] = v.normal.x;
normals[i++] = v.normal.y;
normals[i++] = v.normal.z;
}
return normals;
}
public int getNumFaces() {
return numFaces;
}
public int getNumVertices() {
return numVertices;
}
public TriangleMesh getRotatedAroundAxis(Vec3D axis, float theta) {
return copy().rotateAroundAxis(axis, theta);
}
public TriangleMesh getRotatedX(float theta) {
return copy().rotateX(theta);
}
public TriangleMesh getRotatedY(float theta) {
return copy().rotateY(theta);
}
public TriangleMesh getRotatedZ(float theta) {
return copy().rotateZ(theta);
}
public TriangleMesh getScaled(float scale) {
return copy().scale(scale);
}
public TriangleMesh getScaled(Vec3D scale) {
return copy().scale(scale);
}
public TriangleMesh getTranslated(Vec3D trans) {
return copy().translate(trans);
}
public float[] getUniqueVerticesAsArray() {
float[] verts = new float[numVertices * 3];
int i = 0;
for (Vertex v : vertices.values()) {
verts[i++] = v.x;
verts[i++] = v.y;
verts[i++] = v.z;
}
return verts;
}
public Vertex getVertexAtPoint(Vec3D v) {
return vertices.get(v);
}
public Vertex getVertexForID(int id) {
Vertex vertex = null;
for (Vertex v : vertices.values()) {
if (v.id == id) {
vertex = v;
break;
}
}
return vertex;
}
/**
* Creates an array of unravelled vertex normal coordinates for all faces.
* Uses default stride = 4.
*
* @see #getVertexNormalsAsArray(float[], int, int)
* @return array of xyz normal coords
*/
public float[] getVertexNormalsAsArray() {
return getVertexNormalsAsArray(null, 0, DEFAULT_STRIDE);
}
/**
* Creates an array of unravelled vertex normal coordinates for all faces.
* This method can be used to translate the internal mesh data structure
* into a format suitable for OpenGL Vertex Buffer Objects (by choosing
* stride=4). For more detail, please see
* {@link #getMeshAsVertexArray(float[], int, int)}
*
* @see #getMeshAsVertexArray(float[], int, int)
*
* @param normals
* existing float array or null to automatically create one
* @param offset
* start index in array to place normals
* @param stride
* stride/alignment setting for individual coordinates (min value
* = 3)
* @return array of xyz normal coords
*/
public float[] getVertexNormalsAsArray(float[] normals, int offset,
int stride) {
stride = MathUtils.max(stride, 3);
if (normals == null) {
normals = new float[faces.size() * 3 * stride];
}
int i = offset;
for (Face f : faces) {
normals[i] = f.a.normal.x;
normals[i + 1] = f.a.normal.y;
normals[i + 2] = f.a.normal.z;
i += stride;
normals[i] = f.b.normal.x;
normals[i + 1] = f.b.normal.y;
normals[i + 2] = f.b.normal.z;
i += stride;
normals[i] = f.c.normal.x;
normals[i + 1] = f.c.normal.y;
normals[i + 2] = f.c.normal.z;
i += stride;
}
return normals;
}
public Collection<Vertex> getVertices() {
return vertices.values();
}
protected void handleSaveAsSTL(STLWriter stl, boolean useFlippedY) {
if (useFlippedY) {
stl.setScale(new Vec3D(1, -1, 1));
for (Face f : faces) {
stl.face(f.a, f.b, f.c, f.normal, STLWriter.DEFAULT_RGB);
}
} else {
for (Face f : faces) {
stl.face(f.b, f.a, f.c, f.normal, STLWriter.DEFAULT_RGB);
}
}
stl.endSave();
logger.info(numFaces + " faces written");
}
public TriangleMesh init(String name, int numV, int numF) {
setName(name);
vertices = new LinkedHashMap<Vec3D, Vertex>(numV, 1.5f, false);
faces = new ArrayList<Face>(numF);
return this;
}
public boolean intersectsRay(Ray3D ray) {
Triangle3D tri = intersector.getTriangle();
for (Face f : faces) {
tri.set(f.a, f.b, f.c);
if (intersector.intersectsRay(ray)) {
return true;
}
}
return false;
}
public Triangle3D perforateFace(Face f, float size) {
Vec3D centroid = f.getCentroid();
float d = 1 - size;
Vec3D a2 = f.a.interpolateTo(centroid, d);
Vec3D b2 = f.b.interpolateTo(centroid, d);
Vec3D c2 = f.c.interpolateTo(centroid, d);
removeFace(f);
addFace(f.a, b2, a2);
addFace(f.a, f.b, b2);
addFace(f.b, c2, b2);
addFace(f.b, f.c, c2);
addFace(f.c, a2, c2);
addFace(f.c, f.a, a2);
return new Triangle3D(a2, b2, c2);
}
/**
* Rotates the mesh in such a way so that its "forward" axis is aligned with
* the given direction. This version uses the positive Z-axis as default
* forward direction.
*
* @param dir
* new target direction to point in
* @return itself
*/
public TriangleMesh pointTowards(ReadonlyVec3D dir) {
return transform(Quaternion.getAlignmentQuat(dir, Vec3D.Z_AXIS)
.toMatrix4x4(matrix), true);
}
/**
* Rotates the mesh in such a way so that its "forward" axis is aligned with
* the given direction. This version allows to specify the forward
* direction.
*
* @param dir
* new target direction to point in
* @param forward
* current forward axis
* @return itself
*/
public TriangleMesh pointTowards(ReadonlyVec3D dir, ReadonlyVec3D forward) {
return transform(
Quaternion.getAlignmentQuat(dir, forward).toMatrix4x4(matrix),
true);
}
public void removeFace(Face f) {
faces.remove(f);
}
public TriangleMesh rotateAroundAxis(Vec3D axis, float theta) {
return transform(matrix.identity().rotateAroundAxis(axis, theta));
}
public TriangleMesh rotateX(float theta) {
return transform(matrix.identity().rotateX(theta));
}
public TriangleMesh rotateY(float theta) {
return transform(matrix.identity().rotateY(theta));
}
public TriangleMesh rotateZ(float theta) {
return transform(matrix.identity().rotateZ(theta));
}
/**
* Saves the mesh as OBJ format by appending it to the given mesh
* {@link OBJWriter} instance.
*
* @param obj
*/
public void saveAsOBJ(OBJWriter obj) {
saveAsOBJ(obj, true);
}
public void saveAsOBJ(OBJWriter obj, boolean saveNormals) {
int vOffset = obj.getCurrVertexOffset() + 1;
int nOffset = obj.getCurrNormalOffset() + 1;
logger.info("writing OBJMesh: " + this.toString());
obj.newObject(name);
// vertices
for (Vertex v : vertices.values()) {
obj.vertex(v);
}
// faces
if (saveNormals) {
// normals
for (Vertex v : vertices.values()) {
obj.normal(v.normal);
}
for (Face f : faces) {
obj.faceWithNormals(f.b.id + vOffset, f.a.id + vOffset, f.c.id
+ vOffset, f.b.id + nOffset, f.a.id + nOffset, f.c.id
+ nOffset);
}
} else {
for (Face f : faces) {
obj.face(f.b.id + vOffset, f.a.id + vOffset, f.c.id + vOffset);
}
}
}
/**
* Saves the mesh as OBJ format to the given {@link OutputStream}. Currently
* no texture coordinates are supported or written.
*
* @param stream
*/
public void saveAsOBJ(OutputStream stream) {
OBJWriter obj = new OBJWriter();
obj.beginSave(stream);
saveAsOBJ(obj);
obj.endSave();
}
/**
* Saves the mesh as OBJ format to the given file path. Existing files will
* be overwritten.
*
* @param path
*/
public void saveAsOBJ(String path) {
saveAsOBJ(path, true);
}
public void saveAsOBJ(String path, boolean saveNormals) {
OBJWriter obj = new OBJWriter();
obj.beginSave(path);
saveAsOBJ(obj, saveNormals);
obj.endSave();
}
/**
* Saves the mesh as binary STL format to the given {@link OutputStream}.
*
* @param stream
* @see #saveAsSTL(OutputStream, boolean)
*/
public final void saveAsSTL(OutputStream stream) {
saveAsSTL(stream, false);
}
/**
* Saves the mesh as binary STL format to the given {@link OutputStream}.
* The exported mesh can optionally have it's Y axis flipped by setting the
* useFlippedY flag to true.
*
* @param stream
* @param useFlippedY
*/
public final void saveAsSTL(OutputStream stream, boolean useFlippedY) {
STLWriter stl = new STLWriter();
stl.beginSave(stream, numFaces);
handleSaveAsSTL(stl, useFlippedY);
}
/**
* Saves the mesh as binary STL format to the given {@link OutputStream} and
* using the supplied {@link STLWriter} instance. Use this method to export
* data in a custom {@link STLColorModel}. The exported mesh can optionally
* have it's Y axis flipped by setting the useFlippedY flag to true.
*
* @param stream
* @param stl
* @param useFlippedY
*/
public final void saveAsSTL(OutputStream stream, STLWriter stl,
boolean useFlippedY) {
stl.beginSave(stream, numFaces);
handleSaveAsSTL(stl, useFlippedY);
}
/**
* Saves the mesh as binary STL format to the given file path. Existing
* files will be overwritten.
*
* @param fileName
*/
public final void saveAsSTL(String fileName) {
saveAsSTL(fileName, false);
}
/**
* Saves the mesh as binary STL format to the given file path. The exported
* mesh can optionally have it's Y axis flipped by setting the useFlippedY
* flag to true. Existing files will be overwritten.
*
* @param fileName
* @param useFlippedY
*/
public final void saveAsSTL(String fileName, boolean useFlippedY) {
saveAsSTL(fileName, new STLWriter(), useFlippedY);
}
public final void saveAsSTL(String fileName, STLWriter stl,
boolean useFlippedY) {
stl.beginSave(fileName, numFaces);
handleSaveAsSTL(stl, useFlippedY);
}
public TriangleMesh scale(float scale) {
return transform(matrix.identity().scaleSelf(scale));
}
public TriangleMesh scale(float x, float y, float z) {
return transform(matrix.identity().scaleSelf(x, y, z));
}
public TriangleMesh scale(Vec3D scale) {
return transform(matrix.identity().scaleSelf(scale));
}
public TriangleMesh setName(String name) {
this.name = name;
return this;
}
@Override
public String toString() {
return "TriangleMesh: " + name + " vertices: " + getNumVertices()
+ " faces: " + getNumFaces();
}
public WETriangleMesh toWEMesh() {
return new WETriangleMesh(name, vertices.size(), faces.size())
.addMesh(this);
}
/**
* Applies the given matrix transform to all mesh vertices and updates all
* face normals.
*
* @param mat
* @return itself
*/
public TriangleMesh transform(Matrix4x4 mat) {
return transform(mat, true);
}
/**
* Applies the given matrix transform to all mesh vertices. If the
* updateNormals flag is true, all face normals are updated automatically,
* however vertex normals need a manual update.
*
* @param mat
* @param updateNormals
* @return itself
*/
public TriangleMesh transform(Matrix4x4 mat, boolean updateNormals) {
for (Vertex v : vertices.values()) {
v.set(mat.applyTo(v));
}
if (updateNormals) {
computeFaceNormals();
}
return this;
}
public TriangleMesh translate(float x, float y, float z) {
return transform(matrix.identity().translateSelf(x, y, z));
}
public TriangleMesh translate(Vec3D trans) {
return transform(matrix.identity().translateSelf(trans));
}
public TriangleMesh updateVertex(Vec3D orig, Vec3D newPos) {
Vertex v = vertices.get(orig);
if (v != null) {
vertices.remove(v);
v.set(newPos);
vertices.put(v, v);
}
return this;
}
}