/* * __ .__ .__ ._____. * _/ |_ _______ __|__| ____ | | |__\_ |__ ______ * \ __\/ _ \ \/ / |/ ___\| | | || __ \ / ___/ * | | ( <_> > <| \ \___| |_| || \_\ \\___ \ * |__| \____/__/\_ \__|\___ >____/__||___ /____ > * \/ \/ \/ \/ * * 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 vertices; /** * Face list */ public ArrayList 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); } @Override public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c) { return addFace(a, b, c, null, null, null, null); } @Override public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c, Vec2D uvA, Vec2D uvB, Vec2D uvC) { return addFace(a, b, c, null, uvA, uvB, uvC); } /** * * @param a * @param b * @param c * @param n * @return */ @Override public TriangleMesh addFace(Vec3D a, Vec3D b, Vec3D c, Vec3D n) { return addFace(a, b, c, n, null, null, null); } /** * * @param a * @param b * @param c * @param n * @param uvA * @param uvB * @param uvC * @return */ @Override 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.log(Level.FINE, "ignorning invalid face: {0},{1},{2}", new Object[]{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 * @return */ @Override public TriangleMesh addMesh(Mesh3D m) { m.getFaces().stream().forEach((f) -> { addFace(f.a, f.b, f.c, f.uvA, f.uvB, f.uvC); }); return this; } @Override public AABB center(ReadonlyVec3D origin) { computeCentroid(); Vec3D delta = origin != null ? origin.sub(centroid) : centroid .getInverted(); vertices.values().stream().forEach((v) -> { v.addSelf(delta); }); getBoundingBox(); return bounds; } private 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. */ @Override public TriangleMesh clear() { vertices.clear(); faces.clear(); bounds = null; numVertices = 0; numFaces = 0; uniqueVertexID = 0; return this; } @Override public Vec3D computeCentroid() { centroid.clear(); vertices.values().stream().forEach((v) -> { centroid.addSelf(v); }); return centroid.scaleSelf(1f / numVertices).copy(); } /** * Re-calculates all face normals. */ @Override public TriangleMesh computeFaceNormals() { faces.stream().forEach((f) -> { f.computeNormal(); }); return this; } /** * Computes the smooth vertex normals for the entire mesh. */ @Override public TriangleMesh computeVertexNormals() { vertices.values().stream().forEach((v) -> { v.clearNormal(); }); faces.stream().map((f) -> { f.a.addFaceNormal(f.normal); return f; }).map((f) -> { f.b.addFaceNormal(f.normal); return f; }).forEach((f) -> { f.c.addFaceNormal(f.normal); }); vertices.values().stream().forEach((v) -> { 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); faces.stream().forEach((f) -> { m.addFace(f.a, f.b, f.c, f.normal, f.uvA, f.uvB, f.uvC); }); return m; } /** * * @param v * @param id * @return */ protected Vertex createVertex(Vec3D v, int id) { return new Vertex(v, id); } @Override public TriangleMesh faceOutwards() { computeCentroid(); faces.stream().forEach((f) -> { Vec3D n = f.getCentroid().sub(centroid); float dot = n.dot(f.normal); if (dot < 0) { f.flipVertexOrder(); } }); return this; } @Override public TriangleMesh flipVertexOrder() { faces.stream().map((f) -> { Vertex t = f.a; f.a = f.b; f.b = t; return f; }).map((f) -> { Vec2D tuv = f.uvA; f.uvA = f.uvB; f.uvB = tuv; return f; }).forEach((f) -> { f.normal.invert(); }); return this; } @Override public TriangleMesh flipYAxis() { transform(new Matrix4x4().scaleSelf(1, -1, 1)); flipVertexOrder(); return this; } @Override public AABB getBoundingBox() { final Vec3D minBounds = Vec3D.MAX_VALUE.copy(); final Vec3D maxBounds = Vec3D.NEG_MAX_VALUE.copy(); vertices.values().stream().map((v) -> { minBounds.minSelf(v); return v; }).forEach((v) -> { maxBounds.maxSelf(v); }); bounds = AABB.fromMinMax(minBounds, maxBounds); return bounds; } @Override 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)); } /** * * @param p * @return */ @Override 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; } /** * * @return */ @Override public List 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; } @Override 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: * *
    *
  • Face 1: *
      *
    • Vertex #1 *
        *
      • x
      • *
      • y
      • *
      • z
      • *
      • [optional empty indices to match stride setting]
      • *
      *
    • *
    • Vertex #2 *
        *
      • x
      • *
      • y
      • *
      • z
      • *
      • [optional empty indices to match stride setting]
      • *
      *
    • *
    • Vertex #3 *
        *
      • x
      • *
      • y
      • *
      • z
      • *
      • [optional empty indices to match stride setting]
      • *
      *
    • *
    *
  • Face 2: *
      *
    • Vertex #1
    • *
    • ...etc.
    • *
    *
* * @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; } /** * * @return */ 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; } @Override public int getNumFaces() { return numFaces; } @Override public int getNumVertices() { return numVertices; } /** * * @param axis * @param theta * @return */ public TriangleMesh getRotatedAroundAxis(Vec3D axis, float theta) { return copy().rotateAroundAxis(axis, theta); } /** * * @param theta * @return */ public TriangleMesh getRotatedX(float theta) { return copy().rotateX(theta); } /** * * @param theta * @return */ public TriangleMesh getRotatedY(float theta) { return copy().rotateY(theta); } /** * * @param theta * @return */ public TriangleMesh getRotatedZ(float theta) { return copy().rotateZ(theta); } /** * * @param scale * @return */ public TriangleMesh getScaled(float scale) { return copy().scale(scale); } /** * * @param scale * @return */ public TriangleMesh getScaled(Vec3D scale) { return copy().scale(scale); } /** * * @param trans * @return */ public TriangleMesh getTranslated(Vec3D trans) { return copy().translate(trans); } /** * * @return */ 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; } /** * * @param v * @return */ public Vertex getVertexAtPoint(Vec3D v) { return vertices.get(v); } /** * * @param id * @return */ 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; } /** * * @return */ @Override public Collection getVertices() { return vertices.values(); } /** * * @param stl * @param useFlippedY */ protected void handleSaveAsSTL(STLWriter stl, boolean useFlippedY) { if (useFlippedY) { stl.setScale(new Vec3D(1, -1, 1)); faces.stream().forEach((f) -> { stl.face(f.a, f.b, f.c, f.normal, STLWriter.DEFAULT_RGB); }); } else { faces.stream().forEach((f) -> { stl.face(f.b, f.a, f.c, f.normal, STLWriter.DEFAULT_RGB); }); } stl.endSave(); logger.log(Level.INFO, "{0} faces written", numFaces); } /** * * @param name * @param numV * @param numF * @return */ @Override public TriangleMesh init(String name, int numV, int numF) { setName(name); vertices = new LinkedHashMap<>(numV, 1.5f, false); faces = new ArrayList<>(numF); return this; } @Override public boolean intersectsRay(Ray3D ray) { Triangle3D tri = intersector.getTriangle(); return faces.stream().map((f) -> { tri.set(f.a, f.b, f.c); return f; }).anyMatch((_item) -> (intersector.intersectsRay(ray))); } /** * * @param f * @param size * @return */ public Triangle3D perforateFace(Face f, float size) { Vec3D centrd = f.getCentroid(); float d = 1 - size; Vec3D a2 = f.a.interpolateTo(centrd, d); Vec3D b2 = f.b.interpolateTo(centrd, d); Vec3D c2 = f.c.interpolateTo(centrd, 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); } /** * * @param f */ public void removeFace(Face f) { faces.remove(f); } /** * * @param axis * @param theta * @return */ public TriangleMesh rotateAroundAxis(Vec3D axis, float theta) { return transform(matrix.identity().rotateAroundAxis(axis, theta)); } /** * * @param theta * @return */ public TriangleMesh rotateX(float theta) { return transform(matrix.identity().rotateX(theta)); } /** * * @param theta * @return */ public TriangleMesh rotateY(float theta) { return transform(matrix.identity().rotateY(theta)); } /** * * @param theta * @return */ 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); } /** * * @param obj * @param saveNormals */ public void saveAsOBJ(OBJWriter obj, boolean saveNormals) { int vOffset = obj.getCurrVertexOffset() + 1; int nOffset = obj.getCurrNormalOffset() + 1; logger.log(Level.INFO, "writing OBJMesh: {0}", this.toString()); obj.newObject(name); // vertices vertices.values().stream().forEach((v) -> { obj.vertex(v); }); // faces if (saveNormals) { // normals vertices.values().stream().forEach((v) -> { obj.normal(v.normal); }); faces.stream().forEach((f) -> { 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 { faces.stream().forEach((f) -> { 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); } /** * * @param path * @param saveNormals */ 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); } /** * * @param fileName * @param stl * @param useFlippedY */ public final void saveAsSTL(String fileName, STLWriter stl, boolean useFlippedY) { stl.beginSave(fileName, numFaces); handleSaveAsSTL(stl, useFlippedY); } /** * * @param scale * @return */ public TriangleMesh scale(float scale) { return transform(matrix.identity().scaleSelf(scale)); } /** * * @param x * @param y * @param z * @return */ public TriangleMesh scale(float x, float y, float z) { return transform(matrix.identity().scaleSelf(x, y, z)); } /** * * @param scale * @return */ public TriangleMesh scale(Vec3D scale) { return transform(matrix.identity().scaleSelf(scale)); } /** * * @param name * @return */ @Override public TriangleMesh setName(String name) { this.name = name; return this; } /** * * @return */ @Override public String toString() { return "TriangleMesh: " + name + " vertices: " + getNumVertices() + " faces: " + getNumFaces(); } /** * * @return */ 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) { vertices.values().stream().forEach((v) -> { v.set(mat.applyTo(v)); }); if (updateNormals) { computeFaceNormals(); } return this; } /** * * @param x * @param y * @param z * @return */ public TriangleMesh translate(float x, float y, float z) { return transform(matrix.identity().translateSelf(x, y, z)); } /** * * @param trans * @return */ public TriangleMesh translate(Vec3D trans) { return transform(matrix.identity().translateSelf(trans)); } /** * * @param orig * @param newPos * @return */ 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; } }