/*
* Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.shape;
import com.sun.javafx.scene.shape.ObservableFaceArrayImpl;
import com.sun.javafx.collections.FloatArraySyncer;
import com.sun.javafx.collections.IntegerArraySyncer;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.BoxBounds;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Vec3d;
import com.sun.javafx.scene.input.PickResultChooser;
import com.sun.javafx.scene.shape.TriangleMeshHelper;
import com.sun.javafx.sg.prism.NGTriangleMesh;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ArrayChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableArray;
import javafx.collections.ObservableFloatArray;
import javafx.collections.ObservableIntegerArray;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.Node;
import javafx.scene.input.PickResult;
import javafx.scene.transform.Affine;
import javafx.scene.transform.NonInvertibleTransformException;
import javafx.scene.transform.Rotate;
import com.sun.javafx.logging.PlatformLogger;
Defines a 3D triangle mesh that consists of its associated VertexFormat
and a set of separate arrays of vertex components such as points, normals, texture coordinates, and an array of faces that define the individual triangles of the mesh.
Note that the term point, as used in the method names and method
descriptions, actually refers to a 3D point (x, y, z) in space
representing the position of a single vertex. The term points (plural) is
used to indicate sets of 3D points for multiple vertices.
Similarly, the term normal is used to indicate a 3D vector (nx, ny, nz) in space
representing the direction of a single vertex. The term normals (plural) is
used to indicate sets of 3D vectors for multiple vertices.
The term texCoord is used to indicate a single pair of 2D texture
coordinates (u, v) for a single vertex, while the term texCoords (plural) is used
to indicate sets of texture coordinates for multiple vertices.
Lastly, the term face is used to indicate 3 sets of interleaving points,
normals (optional, depending on the associated VertexFormat)
and texture coordinates that together represent the geometric topology of a
single triangle, while the term faces (plural) is used to indicate sets of
triangles (each represent by a face).
For example, the faces with VertexFormat.POINT_TEXCOORD
that represent a single textured rectangle, using 2 triangles, have the following data order: [
p0, t0, p1, t1, p3, t3, // First triangle of a textured rectangle
p1, t1, p2, t2, p3, t3 // Second triangle of a textured rectangle
]
whereas the faces with VertexFormat.POINT_NORMAL_TEXCOORD
that represent a single textured rectangle, using 2 triangles, have the following data order: [
p0, n0, t0, p1, n1, t1, p3, n3, t3, // First triangle of a textured rectangle
p1, n1, t1, p2, n2, t2, p3, n3, t3 // Second triangle of a textured rectangle
]
where p0, p1, p2 and p3 are indices into the points array, n0, n1, n2 and n3
are indices into the normals array, and t0, t1, t2 and t3 are indices into
the texCoords array.
A triangle has a front and back face. The winding order of a triangle's vertices determines which side is the front face. JavaFX chooses the counter-clockwise (or right-hand rule) winding order as the front face. By default, only the front face of a triangle is rendered. See CullFace
for more information.
The length of points
, normals
, and texCoords
must be divisible by 3, 3, and 2 respectively. The length of faces
must be divisible by 6 if it is of VertexFormat.POINT_TEXCOORD
else it must be divisible by 9 if it is of VertexFormat.POINT_NORMAL_TEXCOORD
. The values in the faces array must be within the range of the number of vertices in the points array (0 to points.length / 3 - 1) for the point indices, within the range of the number of vertices in the normals array (0 to normals.length / 3 - 1) for the normal indices, and within the range of the number of the vertices in the texCoords array (0 to texCoords.length / 2 - 1) for the texture coordinate indices.
A warning will be recorded to the logger and the mesh will not be rendered
(and will have an empty bounds) if any of the array lengths are invalid
or if any of the values in the faces array are out of range.
Since: JavaFX 8.0
/**
* Defines a 3D triangle mesh that consists of its associated {@code VertexFormat}
* and a set of separate arrays of vertex components such as points, normals,
* texture coordinates, and an array of faces that define the individual triangles
* of the mesh.
*<p>
* Note that the term point, as used in the method names and method
* descriptions, actually refers to a 3D point (x, y, z) in space
* representing the position of a single vertex. The term points (plural) is
* used to indicate sets of 3D points for multiple vertices.
* Similarly, the term normal is used to indicate a 3D vector (nx, ny, nz) in space
* representing the direction of a single vertex. The term normals (plural) is
* used to indicate sets of 3D vectors for multiple vertices.
* The term texCoord is used to indicate a single pair of 2D texture
* coordinates (u, v) for a single vertex, while the term texCoords (plural) is used
* to indicate sets of texture coordinates for multiple vertices.
* Lastly, the term face is used to indicate 3 sets of interleaving points,
* normals (optional, depending on the associated VertexFormat)
* and texture coordinates that together represent the geometric topology of a
* single triangle, while the term faces (plural) is used to indicate sets of
* triangles (each represent by a face).
* <p>
* For example, the faces with {@code VertexFormat.POINT_TEXCOORD} that represent
* a single textured rectangle, using 2 triangles, have the following data order: [
* <p>
* p0, t0, p1, t1, p3, t3, // First triangle of a textured rectangle
* <p>
* p1, t1, p2, t2, p3, t3 // Second triangle of a textured rectangle
* <p>
* ]
* <p>
* whereas the faces with {@code VertexFormat.POINT_NORMAL_TEXCOORD} that represent
* a single textured rectangle, using 2 triangles, have the following data order: [
* <p>
* p0, n0, t0, p1, n1, t1, p3, n3, t3, // First triangle of a textured rectangle
* <p>
* p1, n1, t1, p2, n2, t2, p3, n3, t3 // Second triangle of a textured rectangle
* <p>
* ]
* <p>
* where p0, p1, p2 and p3 are indices into the points array, n0, n1, n2 and n3
* are indices into the normals array, and t0, t1, t2 and t3 are indices into
* the texCoords array.
*
* <p>
* A triangle has a front and back face. The winding order of a triangle's vertices
* determines which side is the front face. JavaFX chooses the counter-clockwise
* (or right-hand rule) winding order as the front face. By default, only the
* front face of a triangle is rendered. See {@code CullFace} for more
* information.
*
* <p>
* The length of {@code points}, {@code normals}, and {@code texCoords} must be
* divisible by 3, 3, and 2 respectively. The length of {@code faces} must be
* divisible by 6 if it is of {@code VertexFormat.POINT_TEXCOORD} else it must
* be divisible by 9 if it is of {@code VertexFormat.POINT_NORMAL_TEXCOORD}.
* The values in the faces array must be within the range of the number of vertices
* in the points array (0 to points.length / 3 - 1) for the point indices, within
* the range of the number of vertices in the normals array
* (0 to normals.length / 3 - 1) for the normal indices, and within the range of
* the number of the vertices in the texCoords array (0 to texCoords.length / 2 - 1)
* for the texture coordinate indices.
*
* <p> A warning will be recorded to the logger and the mesh will not be rendered
* (and will have an empty bounds) if any of the array lengths are invalid
* or if any of the values in the faces array are out of range.
*
* @since JavaFX 8.0
*/
public class TriangleMesh extends Mesh {
static {
TriangleMeshHelper.setTriangleMeshAccessor(new TriangleMeshHelper.TriangleMeshAccessor() {
@Override
public boolean doComputeIntersects(Mesh mesh, PickRay pickRay,
PickResultChooser pickResult, Node candidate, CullFace cullFace,
boolean reportFace) {
return ((TriangleMesh) mesh).doComputeIntersects(pickRay,
pickResult, candidate, cullFace, reportFace);
}
});
}
private final ObservableFloatArray points = FXCollections.observableFloatArray();
private final ObservableFloatArray normals = FXCollections.observableFloatArray();
private final ObservableFloatArray texCoords = FXCollections.observableFloatArray();
private final ObservableFaceArray faces = new ObservableFaceArrayImpl();
private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray();
private final Listener pointsSyncer = new Listener(points);
private final Listener normalsSyncer = new Listener(normals);
private final Listener texCoordsSyncer = new Listener(texCoords);
private final Listener facesSyncer = new Listener(faces);
private final Listener faceSmoothingGroupsSyncer = new Listener(faceSmoothingGroups);
private final boolean isPredefinedShape;
private boolean isValidDirty = true;
private boolean isPointsValid, isNormalsValid, isTexCoordsValid, isFacesValid, isFaceSmoothingGroupValid;
private int refCount = 1;
private BaseBounds cachedBounds;
Creates a new instance of TriangleMesh
class with the default VertexFormat.POINT_TEXCOORD
format type. /**
* Creates a new instance of {@code TriangleMesh} class with the default
* {@code VertexFormat.POINT_TEXCOORD} format type.
*/
public TriangleMesh() {
this(false);
TriangleMeshHelper.initHelper(this);
}
Creates a new instance of TriangleMesh
class with the specified VertexFormat
. Params: - vertexFormat – specifies the vertex format type.
Since: JavaFX 8u40
/**
* Creates a new instance of {@code TriangleMesh} class with the specified
* {@code VertexFormat}.
*
* @param vertexFormat specifies the vertex format type.
*
* @since JavaFX 8u40
*/
public TriangleMesh(VertexFormat vertexFormat) {
this(false);
this.setVertexFormat(vertexFormat);
TriangleMeshHelper.initHelper(this);
}
TriangleMesh(boolean isPredefinedShape) {
this.isPredefinedShape = isPredefinedShape;
if (isPredefinedShape) {
isPointsValid = true;
isNormalsValid = true;
isTexCoordsValid = true;
isFacesValid = true;
isFaceSmoothingGroupValid = true;
} else {
isPointsValid = false;
isNormalsValid = false;
isTexCoordsValid = false;
isFacesValid = false;
isFaceSmoothingGroupValid = false;
}
TriangleMeshHelper.initHelper(this);
}
Specifies the vertex format of this TriangleMesh
, one of VertexFormat.POINT_TEXCOORD
or VertexFormat.POINT_NORMAL_TEXCOORD
. @defaultValue VertexFormat.POINT_TEXCOORD Since: JavaFX 8u40
/**
* Specifies the vertex format of this {@code TriangleMesh}, one of
* {@code VertexFormat.POINT_TEXCOORD} or {@code VertexFormat.POINT_NORMAL_TEXCOORD}.
*
* @defaultValue VertexFormat.POINT_TEXCOORD
*
* @since JavaFX 8u40
*/
private ObjectProperty<VertexFormat> vertexFormat;
public final void setVertexFormat(VertexFormat value) {
vertexFormatProperty().set(value);
}
public final VertexFormat getVertexFormat() {
return vertexFormat == null ? VertexFormat.POINT_TEXCOORD : vertexFormat.get();
}
public final ObjectProperty<VertexFormat> vertexFormatProperty() {
if (vertexFormat == null) {
vertexFormat = new SimpleObjectProperty<VertexFormat>(TriangleMesh.this, "vertexFormat") {
@Override
protected void invalidated() {
setDirty(true);
// Need to mark faces and faceSmoothingGroups dirty too.
facesSyncer.setDirty(true);
faceSmoothingGroupsSyncer.setDirty(true);
}
};
}
return vertexFormat;
}
Returns the number of elements that represents a point.
Returns: number of elements
/**
* Returns the number of elements that represents a point.
*
* @return number of elements
*/
public final int getPointElementSize() {
return getVertexFormat().getPointElementSize();
}
Returns the number of elements that represents a normal.
Returns: number of elements Since: JavaFX 8u40
/**
* Returns the number of elements that represents a normal.
*
* @return number of elements
*
* @since JavaFX 8u40
*/
public final int getNormalElementSize() {
return getVertexFormat().getNormalElementSize();
}
Returns the number of elements that represents a texture coordinates.
Returns: number of elements
/**
* Returns the number of elements that represents a texture coordinates.
*
* @return number of elements
*/
public final int getTexCoordElementSize() {
return getVertexFormat().getTexCoordElementSize();
}
Returns the number of elements that represents a face.
Returns: number of elements
/**
* Returns the number of elements that represents a face.
*
* @return number of elements
*/
public final int getFaceElementSize() {
return getVertexFormat().getVertexIndexSize() * 3;
}
Gets the points
array of this TriangleMesh
. Returns: points
array where each point is represented by 3 float values x, y and z, in that order.
/**
* Gets the {@code points} array of this {@code TriangleMesh}.
*
* @return {@code points} array where each point is
* represented by 3 float values x, y and z, in that order.
*/
public final ObservableFloatArray getPoints() {
return points;
}
Gets the normals
array of this TriangleMesh
. Returns: normals
array where each normal is represented by 3 float values nx, ny and nz, in that order.Since: JavaFX 8u40
/**
* Gets the {@code normals} array of this {@code TriangleMesh}.
*
* @return {@code normals} array where each normal is
* represented by 3 float values nx, ny and nz, in that order.
*
* @since JavaFX 8u40
*/
public final ObservableFloatArray getNormals() {
return normals;
}
Gets the texCoords
array of this TriangleMesh
. The coordinates are proportional, so texture's top-left corner is at [0, 0] and bottom-right corner is at [1, 1]. Returns: texCoord
array where each texture coordinate is represented by 2 float values: u and v, in that order.
/**
* Gets the {@code texCoords} array of this {@code TriangleMesh}.
* The coordinates are proportional, so texture's top-left corner
* is at [0, 0] and bottom-right corner is at [1, 1].
*
* @return {@code texCoord} array where each texture coordinate is represented
* by 2 float values: u and v, in that order.
*/
public final ObservableFloatArray getTexCoords() {
return texCoords;
}
Gets the faces
array, indices into the points
, normals
(optional, if it is a VertexFormat.POINT_NORMAL_TEXCOORD
mesh) and texCoords
arrays, of this TriangleMesh
. All indices are in terms of elements in to the points, normals or texCoords arrays not individual floats. Returns: faces
array where each face is of 3 * VertexFormat.getVertexIndexSize()
integers.
/**
* Gets the {@code faces} array, indices into the {@code points},
* {@code normals} (optional, if it is a {@code VertexFormat.POINT_NORMAL_TEXCOORD}
* mesh) and {@code texCoords} arrays, of this {@code TriangleMesh}. All
* indices are in terms of elements in to the points, normals or texCoords
* arrays not individual floats.
*
* @return {@code faces} array where each face is of
* 3 * {@code VertexFormat.getVertexIndexSize()} integers.
*/
public final ObservableFaceArray getFaces() {
return faces;
}
Gets the faceSmoothingGroups
array of this TriangleMesh
. Smoothing affects how a mesh is rendered but it does not effect its geometry. The face smoothing group value is used to control the smoothing between adjacent faces. The face smoothing group value is represented by an array of bits and up to
32 unique groups is possible; (1 << 0) to (1 << 31). The face smoothing
group value can range from 0 (no smoothing group) to all 32 groups. A face
can belong to zero or more smoothing groups. A face is a member of group
N if bit N is set, for example, groups |= (1 << N). A value of 0 implies
no smoothing group or hard edges. Smoothing is applied when adjacent pair of faces shared a smoothing group. Otherwise the faces are rendered with a hard edge between them.
An empty faceSmoothingGroups implies all faces in this mesh have a
smoothing group value of 1.
If faceSmoothingGroups is not empty, its size must
be equal to number of faces.
This faceSmoothingGroups has no effect on its TriangleMesh
if it is of VertexFormat.POINT_NORMAL_TEXCOORD
format.
Returns: the faceSmoothingGroups
array of this TriangleMesh
/**
* Gets the {@code faceSmoothingGroups} array of this {@code TriangleMesh}.
* Smoothing affects how a mesh is rendered but it does not effect its
* geometry. The face smoothing group value is used to control the smoothing
* between adjacent faces.
*
* <p>
* {@literal The face smoothing group value is represented by an array of bits and up to
* 32 unique groups is possible; (1 << 0) to (1 << 31). The face smoothing
* group value can range from 0 (no smoothing group) to all 32 groups. A face
* can belong to zero or more smoothing groups. A face is a member of group
* N if bit N is set, for example, groups |= (1 << N). A value of 0 implies
* no smoothing group or hard edges.}
* Smoothing is applied when adjacent pair of faces shared a smoothing group.
* Otherwise the faces are rendered with a hard edge between them.
*
* <p> An empty faceSmoothingGroups implies all faces in this mesh have a
* smoothing group value of 1.
*
* <p> If faceSmoothingGroups is not empty, its size must
* be equal to number of faces.
*
* <p> This faceSmoothingGroups has no effect on its {@code TriangleMesh} if
* it is of {@code VertexFormat.POINT_NORMAL_TEXCOORD} format.
* @return the {@code faceSmoothingGroups} array of this {@code TriangleMesh}
*/
public final ObservableIntegerArray getFaceSmoothingGroups() {
return faceSmoothingGroups;
}
@Override void setDirty(boolean value) {
super.setDirty(value);
if (!value) { // false
pointsSyncer.setDirty(false);
normalsSyncer.setDirty(false);
texCoordsSyncer.setDirty(false);
facesSyncer.setDirty(false);
faceSmoothingGroupsSyncer.setDirty(false);
}
}
int getRefCount() {
return refCount;
}
synchronized void incRef() {
this.refCount += 1;
}
synchronized void decRef() {
this.refCount -= 1;
}
private NGTriangleMesh peer;
/* The peer node created by the graphics Toolkit/Pipeline implementation */
NGTriangleMesh getPGTriangleMesh() {
if (peer == null) {
peer = new NGTriangleMesh();
}
return peer;
}
@Override
NGTriangleMesh getPGMesh() {
return getPGTriangleMesh();
}
private boolean validatePoints() {
if (points.size() == 0) { // Valid but meaningless for picking or rendering.
return false;
}
if ((points.size() % getVertexFormat().getPointElementSize()) != 0) {
String logname = TriangleMesh.class.getName();
PlatformLogger.getLogger(logname).warning("points.size() has "
+ "to be divisible by getPointElementSize(). It is to"
+ " store multiple x, y, and z coordinates of this mesh");
return false;
}
return true;
}
private boolean validateNormals() {
// Only validate normals if vertex format has normal component
if (getVertexFormat() != VertexFormat.POINT_NORMAL_TEXCOORD) return true;
if (normals.size() == 0) { // Valid but meaningless for picking or rendering.
return false;
}
if ((normals.size() % getVertexFormat().getNormalElementSize()) != 0) {
String logname = TriangleMesh.class.getName();
PlatformLogger.getLogger(logname).warning("normals.size() has "
+ "to be divisible by getNormalElementSize(). It is to"
+ " store multiple nx, ny, and nz coordinates of this mesh");
return false;
}
return true;
}
private boolean validateTexCoords() {
if (texCoords.size() == 0) { // Valid but meaningless for picking or rendering.
return false;
}
if ((texCoords.size() % getVertexFormat().getTexCoordElementSize()) != 0) {
String logname = TriangleMesh.class.getName();
PlatformLogger.getLogger(logname).warning("texCoords.size() "
+ "has to be divisible by getTexCoordElementSize()."
+ " It is to store multiple u and v texture coordinates"
+ " of this mesh");
return false;
}
return true;
}
private boolean validateFaces() {
if (faces.size() == 0) { // Valid but meaningless for picking or rendering.
return false;
}
String logname = TriangleMesh.class.getName();
if ((faces.size() % getFaceElementSize()) != 0) {
PlatformLogger.getLogger(logname).warning("faces.size() has "
+ "to be divisible by getFaceElementSize().");
return false;
}
if (getVertexFormat() == VertexFormat.POINT_TEXCOORD) {
int nVerts = points.size() / getVertexFormat().getPointElementSize();
int nTVerts = texCoords.size() / getVertexFormat().getTexCoordElementSize();
for (int i = 0; i < faces.size(); i++) {
if (i % 2 == 0 && (faces.get(i) >= nVerts || faces.get(i) < 0)
|| (i % 2 != 0 && (faces.get(i) >= nTVerts || faces.get(i) < 0))) {
PlatformLogger.getLogger(logname).warning("The values in the "
+ "faces array must be within the range of the number "
+ "of vertices in the points array (0 to points.length / 3 - 1) "
+ "for the point indices and within the range of the "
+ "number of the vertices in the texCoords array (0 to "
+ "texCoords.length / 2 - 1) for the texture coordinate indices.");
return false;
}
}
} else if (getVertexFormat() == VertexFormat.POINT_NORMAL_TEXCOORD) {
int nVerts = points.size() / getVertexFormat().getPointElementSize();
int nNVerts = normals.size() / getVertexFormat().getNormalElementSize();
int nTVerts = texCoords.size() / getVertexFormat().getTexCoordElementSize();
for (int i = 0; i < faces.size(); i+=3) {
if ((faces.get(i) >= nVerts || faces.get(i) < 0)
|| (faces.get(i + 1) >= nNVerts || faces.get(i + 1) < 0)
|| (faces.get(i + 2) >= nTVerts || faces.get(i + 2) < 0)) {
PlatformLogger.getLogger(logname).warning("The values in the "
+ "faces array must be within the range of the number "
+ "of vertices in the points array (0 to points.length / 3 - 1) "
+ "for the point indices, and within the range of the "
+ "number of the vertices in the normals array (0 to "
+ "normals.length / 3 - 1) for the normals indices, and "
+ "number of the vertices in the texCoords array (0 to "
+ "texCoords.length / 2 - 1) for the texture coordinate indices.");
return false;
}
}
} else {
PlatformLogger.getLogger(logname).warning("Unsupported VertexFormat: " + getVertexFormat().toString());
return false;
}
return true;
}
private boolean validateFaceSmoothingGroups() {
if (faceSmoothingGroups.size() != 0
&& faceSmoothingGroups.size() != (faces.size() / getFaceElementSize())) {
String logname = TriangleMesh.class.getName();
PlatformLogger.getLogger(logname).warning("faceSmoothingGroups.size()"
+ " has to equal to number of faces.");
return false;
}
return true;
}
private boolean validate() {
if (isPredefinedShape) {
return true;
}
if (isValidDirty) {
if (pointsSyncer.dirtyInFull) {
isPointsValid = validatePoints();
}
if (normalsSyncer.dirtyInFull) {
isNormalsValid = validateNormals();
}
if (texCoordsSyncer.dirtyInFull) {
isTexCoordsValid = validateTexCoords();
}
if (facesSyncer.dirty || pointsSyncer.dirtyInFull
|| normalsSyncer.dirtyInFull || texCoordsSyncer.dirtyInFull) {
isFacesValid = isPointsValid && isNormalsValid
&& isTexCoordsValid && validateFaces();
}
if (faceSmoothingGroupsSyncer.dirtyInFull || facesSyncer.dirtyInFull) {
isFaceSmoothingGroupValid = isFacesValid && validateFaceSmoothingGroups();
}
isValidDirty = false;
}
return isPointsValid && isNormalsValid && isTexCoordsValid
&& isFaceSmoothingGroupValid && isFacesValid;
}
@Override
void updatePG() {
if (!isDirty()) {
return;
}
final NGTriangleMesh pgTriMesh = getPGTriangleMesh();
if (validate()) {
pgTriMesh.setUserDefinedNormals(getVertexFormat() == VertexFormat.POINT_NORMAL_TEXCOORD);
pgTriMesh.syncPoints(pointsSyncer);
pgTriMesh.syncNormals(normalsSyncer);
pgTriMesh.syncTexCoords(texCoordsSyncer);
pgTriMesh.syncFaces(facesSyncer);
pgTriMesh.syncFaceSmoothingGroups(faceSmoothingGroupsSyncer);
} else {
pgTriMesh.setUserDefinedNormals(false);
pgTriMesh.syncPoints(null);
pgTriMesh.syncNormals(null);
pgTriMesh.syncTexCoords(null);
pgTriMesh.syncFaces(null);
pgTriMesh.syncFaceSmoothingGroups(null);
}
setDirty(false);
}
@Override
BaseBounds computeBounds(BaseBounds bounds) {
if (isDirty() || cachedBounds == null) {
cachedBounds = new BoxBounds();
if (validate()) {
final int len = points.size();
final int pointElementSize = getVertexFormat().getPointElementSize();
for (int i = 0; i < len; i += pointElementSize) {
cachedBounds.add(points.get(i), points.get(i + 1), points.get(i + 2));
}
}
}
return bounds.deriveWithNewBounds(cachedBounds);
}
Computes the centroid of the given triangle
Params: - v0x – x coord of first vertex of the triangle
- v0y – y coord of first vertex of the triangle
- v0z – z coord of first vertex of the triangle
- v1x – x coord of second vertex of the triangle
- v1y – y coord of second vertex of the triangle
- v1z – z coord of second vertex of the triangle
- v2x – x coord of third vertex of the triangle
- v2y – y coord of third vertex of the triangle
- v2z – z coord of third vertex of the triangle
Returns: the triangle centroid
/**
* Computes the centroid of the given triangle
* @param v0x x coord of first vertex of the triangle
* @param v0y y coord of first vertex of the triangle
* @param v0z z coord of first vertex of the triangle
* @param v1x x coord of second vertex of the triangle
* @param v1y y coord of second vertex of the triangle
* @param v1z z coord of second vertex of the triangle
* @param v2x x coord of third vertex of the triangle
* @param v2y y coord of third vertex of the triangle
* @param v2z z coord of third vertex of the triangle
* @return the triangle centroid
*/
private Point3D computeCentroid(
double v0x, double v0y, double v0z,
double v1x, double v1y, double v1z,
double v2x, double v2y, double v2z) {
// Point3D center = v1.midpoint(v2);
// Point3D vec = center.subtract(v0);
// return v0.add(new Point3D(vec.getX() / 3.0, vec.getY() / 3.0, vec.getZ() / 3.0));
return new Point3D(
v0x + (v2x + (v1x - v2x) / 2.0 - v0x) / 3.0,
v0y + (v2y + (v1y - v2y) / 2.0 - v0y) / 3.0,
v0z + (v2z + (v1z - v2z) / 2.0 - v0z) / 3.0);
}
Computes the centroid of the given triangle
Params: - v0 – vertex of the triangle
- v1 – vertex of the triangle
- v2 – vertex of the triangle
Returns: the triangle centroid
/**
* Computes the centroid of the given triangle
* @param v0 vertex of the triangle
* @param v1 vertex of the triangle
* @param v2 vertex of the triangle
* @return the triangle centroid
*/
private Point2D computeCentroid(Point2D v0, Point2D v1, Point2D v2) {
Point2D center = v1.midpoint(v2);
Point2D vec = center.subtract(v0);
return v0.add(new Point2D(vec.getX() / 3.0, vec.getY() / 3.0));
}
Computes intersection of a pick ray and a single triangle face.
It takes pickRay, origin and dir. The latter two can be of course obtained
from the pickRay, but we need them to be converted to Point3D and don't
want to do that for all faces. Therefore the conversion is done just once
and passed to the method for all the faces.
Params: - pickRay – pick ray
- origin – pick ray's origin
- dir – pick ray's direction
- faceIndex – index of the face to test
- cullFace – cull face of the Node (and thus the tested face)
- candidate – the owner node (for the possible placement to the result)
- reportFace – whether or not to report he hit face
- result – the pick result to be updated if a closer intersection is found
Returns: true if the pick ray intersects with the face (regardless of whether
the result has been updated)
/**
* Computes intersection of a pick ray and a single triangle face.
*
* It takes pickRay, origin and dir. The latter two can be of course obtained
* from the pickRay, but we need them to be converted to Point3D and don't
* want to do that for all faces. Therefore the conversion is done just once
* and passed to the method for all the faces.
*
* @param pickRay pick ray
* @param origin pick ray's origin
* @param dir pick ray's direction
* @param faceIndex index of the face to test
* @param cullFace cull face of the Node (and thus the tested face)
* @param candidate the owner node (for the possible placement to the result)
* @param reportFace whether or not to report he hit face
* @param result the pick result to be updated if a closer intersection is found
* @return true if the pick ray intersects with the face (regardless of whether
* the result has been updated)
*/
private boolean computeIntersectsFace(
PickRay pickRay, Vec3d origin, Vec3d dir, int faceIndex,
CullFace cullFace, Node candidate, boolean reportFace, PickResultChooser result) {//, BoxBounds rayBounds) {
// This computation was naturally done by Point3D and its operations,
// but it needs a lot of points and there is often a lot of triangles
// so it is vital for performance to use only primitive variables
// and do the computing manually.
int vertexIndexSize = getVertexFormat().getVertexIndexSize();
int pointElementSize = getVertexFormat().getPointElementSize();
final int v0Idx = faces.get(faceIndex) * pointElementSize;
final int v1Idx = faces.get(faceIndex + vertexIndexSize) * pointElementSize;
final int v2Idx = faces.get(faceIndex + (2 * vertexIndexSize)) * pointElementSize;
final float v0x = points.get(v0Idx);
final float v0y = points.get(v0Idx + 1);
final float v0z = points.get(v0Idx + 2);
final float v1x = points.get(v1Idx);
final float v1y = points.get(v1Idx + 1);
final float v1z = points.get(v1Idx + 2);
final float v2x = points.get(v2Idx);
final float v2y = points.get(v2Idx + 1);
final float v2z = points.get(v2Idx + 2);
// e1 = v1.subtract(v0)
final float e1x = v1x - v0x;
final float e1y = v1y - v0y;
final float e1z = v1z - v0z;
// e2 = v2.subtract(v0)
final float e2x = v2x - v0x;
final float e2y = v2y - v0y;
final float e2z = v2z - v0z;
// h = dir.crossProduct(e2)
final double hx = dir.y * e2z - dir.z * e2y;
final double hy = dir.z * e2x - dir.x * e2z;
final double hz = dir.x * e2y - dir.y * e2x;
// a = e1.dotProduct(h)
final double a = e1x * hx + e1y * hy + e1z * hz;
if (a == 0.0) {
return false;
}
final double f = 1.0 / a;
// s = origin.subtract(v0)
final double sx = origin.x - v0x;
final double sy = origin.y - v0y;
final double sz = origin.z - v0z;
// u = f * (s.dotProduct(h))
final double u = f * (sx * hx + sy * hy + sz * hz);
if (u < 0.0 || u > 1.0) {
return false;
}
// q = s.crossProduct(e1)
final double qx = sy * e1z - sz * e1y;
final double qy = sz * e1x - sx * e1z;
final double qz = sx * e1y - sy * e1x;
// v = f * dir.dotProduct(q)
double v = f * (dir.x * qx + dir.y * qy + dir.z * qz);
if (v < 0.0 || u + v > 1.0) {
return false;
}
// t = f * e2.dotProduct(q)
final double t = f * (e2x * qx + e2y * qy + e2z * qz);
if (t >= pickRay.getNearClip() && t <= pickRay.getFarClip()) {
// This branch is entered only for hit triangles (not so often),
// so we can get smoothly back to the nice code using Point3Ds.
if (cullFace != CullFace.NONE) {
// normal = e1.crossProduct(e2)
final Point3D normal = new Point3D(
e1y * e2z - e1z * e2y,
e1z * e2x - e1x * e2z,
e1x * e2y - e1y * e2x);
final double nangle = normal.angle(
new Point3D(-dir.x, -dir.y, -dir.z));
if ((nangle >= 90 || cullFace != CullFace.BACK) &&
(nangle <= 90 || cullFace != CullFace.FRONT)) {
// hit culled face
return false;
}
}
if (Double.isInfinite(t) || Double.isNaN(t)) {
// we've got a nonsense pick ray or triangle
return false;
}
if (result == null || !result.isCloser(t)) {
// it intersects, but we are not interested in the result
// or we already have a better (closer) result
// so we can omit the point and texture computation
return true;
}
Point3D point = PickResultChooser.computePoint(pickRay, t);
// Now compute texture mapping. First rotate the triangle
// so that we can compute in 2D
// centroid = computeCentroid(v0, v1, v2);
final Point3D centroid = computeCentroid(
v0x, v0y, v0z,
v1x, v1y, v1z,
v2x, v2y, v2z);
// cv0 = v0.subtract(centroid)
final Point3D cv0 = new Point3D(
v0x - centroid.getX(),
v0y - centroid.getY(),
v0z - centroid.getZ());
// cv1 = v1.subtract(centroid)
final Point3D cv1 = new Point3D(
v1x - centroid.getX(),
v1y - centroid.getY(),
v1z - centroid.getZ());
// cv2 = v2.subtract(centroid)
final Point3D cv2 = new Point3D(
v2x - centroid.getX(),
v2y - centroid.getY(),
v2z - centroid.getZ());
final Point3D ce1 = cv1.subtract(cv0);
final Point3D ce2 = cv2.subtract(cv0);
Point3D n = ce1.crossProduct(ce2);
if (n.getZ() < 0) {
n = new Point3D(-n.getX(), -n.getY(), -n.getZ());
}
final Point3D ax = n.crossProduct(Rotate.Z_AXIS);
final double angle = Math.atan2(ax.magnitude(), n.dotProduct(Rotate.Z_AXIS));
Rotate r = new Rotate(Math.toDegrees(angle), ax);
final Point3D crv0 = r.transform(cv0);
final Point3D crv1 = r.transform(cv1);
final Point3D crv2 = r.transform(cv2);
final Point3D rPoint = r.transform(point.subtract(centroid));
final Point2D flatV0 = new Point2D(crv0.getX(), crv0.getY());
final Point2D flatV1 = new Point2D(crv1.getX(), crv1.getY());
final Point2D flatV2 = new Point2D(crv2.getX(), crv2.getY());
final Point2D flatPoint = new Point2D(rPoint.getX(), rPoint.getY());
// Obtain the texture triangle
int texCoordElementSize = getVertexFormat().getTexCoordElementSize();
int texCoordOffset = getVertexFormat().getTexCoordIndexOffset();
final int t0Idx = faces.get(faceIndex + texCoordOffset) * texCoordElementSize;
final int t1Idx = faces.get(faceIndex + vertexIndexSize + texCoordOffset) * texCoordElementSize;
final int t2Idx = faces.get(faceIndex + (vertexIndexSize * 2) + texCoordOffset) * texCoordElementSize;
final Point2D u0 = new Point2D(texCoords.get(t0Idx), texCoords.get(t0Idx + 1));
final Point2D u1 = new Point2D(texCoords.get(t1Idx), texCoords.get(t1Idx + 1));
final Point2D u2 = new Point2D(texCoords.get(t2Idx), texCoords.get(t2Idx + 1));
final Point2D txCentroid = computeCentroid(u0, u1, u2);
final Point2D cu0 = u0.subtract(txCentroid);
final Point2D cu1 = u1.subtract(txCentroid);
final Point2D cu2 = u2.subtract(txCentroid);
// Find the transform between the two triangles
final Affine src = new Affine(
flatV0.getX(), flatV1.getX(), flatV2.getX(),
flatV0.getY(), flatV1.getY(), flatV2.getY());
final Affine trg = new Affine(
cu0.getX(), cu1.getX(), cu2.getX(),
cu0.getY(), cu1.getY(), cu2.getY());
Point2D txCoords = null;
try {
src.invert();
trg.append(src);
txCoords = txCentroid.add(trg.transform(flatPoint));
} catch (NonInvertibleTransformException e) {
// Can't compute texture mapping, probably the coordinates
// don't make sense. Ignore it and return null tex coords.
}
result.offer(candidate, t,
reportFace ? faceIndex / getFaceElementSize() : PickResult.FACE_UNDEFINED,
point, txCoords);
return true;
}
return false;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult,
Node candidate, CullFace cullFace, boolean reportFace) {
boolean found = false;
if (validate()) {
final int size = faces.size();
final Vec3d o = pickRay.getOriginNoClone();
final Vec3d d = pickRay.getDirectionNoClone();
for (int i = 0; i < size; i += getFaceElementSize()) {
if (computeIntersectsFace(pickRay, o, d, i, cullFace, candidate,
reportFace, pickResult)) {
found = true;
}
}
}
return found;
}
private class Listener<T extends ObservableArray<T>> implements ArrayChangeListener<T>, FloatArraySyncer, IntegerArraySyncer {
protected final T array;
protected boolean dirty = true;
Array was replaced
Returns: true if array was replaced; false otherwise
/**
* Array was replaced
* @return true if array was replaced; false otherwise
*/
protected boolean dirtyInFull = true;
protected int dirtyRangeFrom;
protected int dirtyRangeLength;
public Listener(T array) {
this.array = array;
array.addListener(this);
}
Adds a dirty range
Params: - from – index of the first modified element
- length – length of the modified range
/**
* Adds a dirty range
* @param from index of the first modified element
* @param length length of the modified range
*/
protected final void addDirtyRange(int from, int length) {
if (length > 0 && !dirtyInFull) {
markDirty();
if (dirtyRangeLength == 0) {
dirtyRangeFrom = from;
dirtyRangeLength = length;
} else {
int fromIndex = Math.min(dirtyRangeFrom, from);
int toIndex = Math.max(dirtyRangeFrom + dirtyRangeLength, from + length);
dirtyRangeFrom = fromIndex;
dirtyRangeLength = toIndex - fromIndex;
}
}
}
protected void markDirty() {
dirty = true;
TriangleMesh.this.setDirty(true);
}
@Override
public void onChanged(T observableArray, boolean sizeChanged, int from, int to) {
if (sizeChanged) {
setDirty(true);
} else {
addDirtyRange(from, to - from);
}
isValidDirty = true;
}
Params: - dirty – if true, the whole collection is marked as dirty;
if false, the whole collection is marked as not-dirty
/**
* @param dirty if true, the whole collection is marked as dirty;
* if false, the whole collection is marked as not-dirty
*/
public final void setDirty(boolean dirty) {
this.dirtyInFull = dirty;
if (dirty) {
markDirty();
dirtyRangeFrom = 0;
dirtyRangeLength = array.size();
} else {
this.dirty = false;
dirtyRangeFrom = dirtyRangeLength = 0;
}
}
@Override
public float[] syncTo(float[] array, int[] fromAndLengthIndices) {
assert ((fromAndLengthIndices != null) && (fromAndLengthIndices.length == 2));
ObservableFloatArray floatArray = (ObservableFloatArray) this.array;
if (dirtyInFull || array == null || array.length != floatArray.size()) {
// Always allocate a new array when size changes
fromAndLengthIndices[0] = 0;
fromAndLengthIndices[1] = floatArray.size();
return floatArray.toArray(null);
}
fromAndLengthIndices[0] = dirtyRangeFrom;
fromAndLengthIndices[1] = dirtyRangeLength;
floatArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
return array;
}
@Override
public int[] syncTo(int[] array, int[] fromAndLengthIndices) {
assert ((fromAndLengthIndices != null) && (fromAndLengthIndices.length == 2));
ObservableIntegerArray intArray = (ObservableIntegerArray) this.array;
if (dirtyInFull || array == null || array.length != intArray.size()) {
fromAndLengthIndices[0] = 0;
fromAndLengthIndices[1] = intArray.size();
// Always allocate a new array when size changes
return intArray.toArray(null);
}
fromAndLengthIndices[0] = dirtyRangeFrom;
fromAndLengthIndices[1] = dirtyRangeLength;
intArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
return array;
}
}
}