All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.shape.TriangleMesh Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2013, 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.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.sg.PGTriangleMesh;
import com.sun.javafx.tk.Toolkit;
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;

/**
 * Defines a 3D geometric object contains separate arrays of points, 
 * texture coordinates, and faces that describe a triangulated 
 * geometric mesh.
 *

* Note that the term point, as used in the method names and method * descriptions, actually refers to a set of x, y, and z point * representing the position of a single vertex. The term points (plural) is * used to indicate sets of x, y, and z points for multiple vertices. * Similarly, the term texCoord is used to indicate a set of u and v texture * coordinates for a single vertex, while the term texCoords (plural) is used * to indicate sets of u and v texture coordinates for multiple vertices. * Lastly, the term face is used to indicate 3 set of interleaving points * 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 that represent a single textured rectangle, using 2 triangles, * has 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 *

* ] *

* where p0, p1, p2 and p3 are indices into the points array, and t0, t1, t2 * and t3 are indices into the texCoords array. * *

* The length of {@code points}, {@code texCoords}, and {@code faces} must be * divisible by 3, 2, and 6 respectively. * 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. * *

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 { public static final int NUM_COMPONENTS_PER_POINT = 3; public static final int NUM_COMPONENTS_PER_TEXCOORD = 2; public static final int NUM_COMPONENTS_PER_FACE = 6; // TODO: 3D - Need to validate the size and range of these arrays. // A warning will be recorded to the logger and the mesh will have an empty // bounds if the validation failed. (RT-30451) // The values in faces must be within range and the length of points, // texCoords and faces must be divisible by 3, 2 and 6 respectively. private final ObservableFloatArray points = FXCollections.observableFloatArray(); private final ObservableFloatArray texCoords = FXCollections.observableFloatArray(); private final ObservableIntegerArray faces = FXCollections.observableIntegerArray(); private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray(); private final Listener pointsSyncer = new Listener(points); private final Listener texCoordsSyncer = new Listener(texCoords); private final Listener facesSyncer = new Listener(faces); private final Listener faceSmoothingGroupsSyncer = new Listener(faceSmoothingGroups); private int refCount = 1; private BaseBounds cachedBounds; /** * Creates a new instance of {@code TriangleMesh} class. */ public TriangleMesh() { } /** * Gets the {@code ObservableFloatArray} of points of this {@code TriangleMesh}. * * @return {@code ObservableFloatArray} of points where each point is * represented by 3 float values x, y and z, in that order. */ public ObservableFloatArray getPoints() { return points; } /** * Gets the {@code ObservableFloatArray} of texture coordinates of this {@code TriangleMesh}. * * @return {@code ObservableFloatArray} array of texture coordinates * where each texture coordinate is represented by 2 float values: u and v, * in that order */ public ObservableFloatArray getTexCoords() { return texCoords; } /** * Gets the {@code ObservableIntegerArray} of faces, indices into the points * and texCoords arrays, of this {@code TriangleMesh} * * @return {@code ObservableIntegerArray} of faces where each face is * 6 integers p0, t0, p1, t1, p3, t3, where p0, p1 and p2 are indices of * points in points {@code ObservableFloatArray} and t0, t1 and t2 are * indices of texture coordinates in texCoords {@code ObservableFloatArray}. * Both indices are in terms of vertices (points or texCoords), not individual * floats. */ public ObservableIntegerArray getFaces() { return faces; } /** * Gets the {@code ObservableIntegerArray} of face smoothing groups * 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. * *

The face smoothing group 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. * *

Note: If faceSmoothingGroups is not empty, is size must * be equal to number of faces. */ public ObservableIntegerArray getFaceSmoothingGroups() { return faceSmoothingGroups; } @Override void setDirty(boolean value) { super.setDirty(value); if (!value) { // false pointsSyncer.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 PGTriangleMesh peer; /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated /** The peer node created by the graphics Toolkit/Pipeline implementation */ PGTriangleMesh impl_getPGTriangleMesh() { if (peer == null) { peer = Toolkit.getToolkit().createPGTriangleMesh(); } return peer; } @Override PGTriangleMesh getPGMesh() { return impl_getPGTriangleMesh(); } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated @Override void impl_updatePG() { if (!isDirty()) { return; } PGTriangleMesh pgTriMesh = impl_getPGTriangleMesh(); // sync points if (pointsSyncer.dirty) { pgTriMesh.syncPoints(pointsSyncer); } if (texCoordsSyncer.dirty) { pgTriMesh.syncTexCoords(texCoordsSyncer); } if (facesSyncer.dirty) { pgTriMesh.syncFaces(facesSyncer); } if (faceSmoothingGroupsSyncer.dirty) { pgTriMesh.syncFaceSmoothingGroups(faceSmoothingGroupsSyncer); } setDirty(false); } @Override BaseBounds computeBounds(BaseBounds bounds) { if (isDirty() || cachedBounds == null) { cachedBounds = new BoxBounds(); final double len = points.size(); for (int i = 0; i < len; i += NUM_COMPONENTS_PER_POINT) { cachedBounds.add(points.get(i), points.get(i + 1), points.get(i + 2)); } } return bounds.deriveWithNewBounds(cachedBounds); } /** * 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 Point3D computeCentroid(Point3D v0, Point3D v1, Point3D v2) { 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)); } /** * 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. * * @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, Point3D origin, Point3D dir, int faceIndex, CullFace cullFace, Node candidate, boolean reportFace, PickResultChooser result) { final int v0Idx = faces.get(faceIndex) * NUM_COMPONENTS_PER_POINT; final int v1Idx = faces.get(faceIndex + 2) * NUM_COMPONENTS_PER_POINT; final int v2Idx = faces.get(faceIndex + 4) * NUM_COMPONENTS_PER_POINT; final Point3D v0 = new Point3D(points.get(v0Idx), points.get(v0Idx + 1), points.get(v0Idx + 2)); final Point3D v1 = new Point3D(points.get(v1Idx), points.get(v1Idx + 1), points.get(v1Idx + 2)); final Point3D v2 = new Point3D(points.get(v2Idx), points.get(v2Idx + 1), points.get(v2Idx + 2)); final Point3D e1 = v1.subtract(v0); final Point3D e2 = v2.subtract(v0); final Point3D h = dir.crossProduct(e2); final double a = e1.dotProduct(h); if (a == 0.0) { return false; } final double f = 1.0 / a; final Point3D s = origin.subtract(v0); final double u = f * (s.dotProduct(h)); if (u < 0.0 || u > 1.0) { return false; } Point3D q = s.crossProduct(e1); double v = f * dir.dotProduct(q); if (v < 0.0 || u + v > 1.0) { return false; } final double t = f * e2.dotProduct(q); if (t >= pickRay.getNearClip() && t <= pickRay.getFarClip()) { if (cullFace != CullFace.NONE) { final Point3D normal = e1.crossProduct(e2); final double nangle = normal.angle( new Point3D(-dir.getX(), -dir.getY(), -dir.getZ())); 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 final Point3D centroid = computeCentroid(v0, v1, v2); final Point3D cv0 = v0.subtract(centroid); final Point3D cv1 = v1.subtract(centroid); final Point3D cv2 = v2.subtract(centroid); 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 final int t0Idx = faces.get(faceIndex + 1) * NUM_COMPONENTS_PER_TEXCOORD; final int t1Idx = faces.get(faceIndex + 3) * NUM_COMPONENTS_PER_TEXCOORD; final int t2Idx = faces.get(faceIndex + 5) * NUM_COMPONENTS_PER_TEXCOORD; 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 / NUM_COMPONENTS_PER_FACE : PickResult.FACE_UNDEFINED, point, txCoords); return true; } return false; } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Override @Deprecated protected boolean impl_computeIntersects(PickRay pickRay, PickResultChooser pickResult, Node candidate, CullFace cullFace, boolean reportFace) { boolean found = false; final int size = faces.size(); final Vec3d o = pickRay.getOriginNoClone(); final Point3D origin = new Point3D(o.x, o.y, o.z); final Vec3d d = pickRay.getDirectionNoClone(); final Point3D dir = new Point3D(d.x, d.y, d.z); for (int i = 0; i < size; i += NUM_COMPONENTS_PER_FACE) { if (computeIntersectsFace(pickRay, origin, dir, i, cullFace, candidate, reportFace, pickResult)) { found = true; } } return found; } private class Listener> implements ArrayChangeListener, FloatArraySyncer, IntegerArraySyncer { protected final T array; protected boolean dirty = true; /** * 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 * @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); } } /** * @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) { ObservableFloatArray floatArray = (ObservableFloatArray) this.array; if (dirtyInFull || array == null || array.length != floatArray.size()) { // Always allocate a new array when size changes return floatArray.toArray(null); } floatArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength); return array; } @Override public int[] syncTo(int[] array) { ObservableIntegerArray intArray = (ObservableIntegerArray) this.array; if (dirtyInFull || array == null || array.length != intArray.size()) { // Always allocate a new array when size changes return intArray.toArray(null); } intArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength); return array; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy