Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.shape;
027
028import com.sun.javafx.collections.FloatArraySyncer;
029import com.sun.javafx.collections.IntegerArraySyncer;
030import com.sun.javafx.geom.BaseBounds;
031import com.sun.javafx.geom.BoxBounds;
032import com.sun.javafx.geom.PickRay;
033import com.sun.javafx.geom.Vec3d;
034import com.sun.javafx.scene.input.PickResultChooser;
035import com.sun.javafx.sg.PGTriangleMesh;
036import com.sun.javafx.tk.Toolkit;
037import javafx.collections.ArrayChangeListener;
038import javafx.collections.FXCollections;
039import javafx.collections.ObservableArray;
040import javafx.collections.ObservableFloatArray;
041import javafx.collections.ObservableIntegerArray;
042import javafx.geometry.Point2D;
043import javafx.geometry.Point3D;
044import javafx.scene.Node;
045import javafx.scene.input.PickResult;
046import javafx.scene.transform.Affine;
047import javafx.scene.transform.NonInvertibleTransformException;
048import javafx.scene.transform.Rotate;
049
050/**
051 * Defines a 3D geometric object contains separate arrays of points, 
052 * texture coordinates, and faces that describe a triangulated 
053 * geometric mesh.
054 *<p>
055 * Note that the term point, as used in the method names and method
056 * descriptions, actually refers to a set of x, y, and z point
057 * representing the position of a single vertex. The term points (plural) is
058 * used to indicate sets of x, y, and z points for multiple vertices.
059 * Similarly, the term texCoord is used to indicate a set of u and v texture
060 * coordinates for a single vertex, while the term texCoords (plural) is used
061 * to indicate sets of u and v texture coordinates for multiple vertices.
062 * Lastly, the term face is used to indicate 3 set of interleaving points
063 * and texture coordinates that together represent the geometric topology of a 
064 * single triangle, while the term faces (plural) is used to indicate sets of 
065 * triangles (each represent by a face).
066 * <p>
067 * For example, the faces that represent a single textured rectangle, using 2 triangles,
068 * has the following data order: [
069 * <p>
070 * p0, t0, p1, t1, p3, t3,  // First triangle of a textured rectangle
071 * <p>
072 * p1, t1, p2, t2, p3, t3   // Second triangle of a textured rectangle
073 * <p>
074 * ]
075 * <p>
076 * where p0, p1, p2 and p3 are indices into the points array, and t0, t1, t2
077 * and t3 are indices into the texCoords array.
078 * 
079 * <p> The maximum number of vertices in the mesh can not exceed 65536.
080 * Therefore the maximum array lengths are 65536 * 3 (x, y, z per point) for points,
081 * and 65536 * 2 (u, v per texture coordinate) for texCoords. 
082 * The length of {@code points}, {@code texCoords}, and {@code faces} must be
083 * divisible by 3, 2, and 6 respectively.
084 * The values in the faces array must be within the range of the number of vertices
085 * in the points array (0 to points.length / 3 - 1) for the point indices and 
086 * within the range of the number of the vertices in 
087 * the texCoords array (0 to texCoords.length / 2 - 1) for the texture coordinate indices.
088 * 
089 * <p> A warning will be recorded to the logger and the mesh will not be rendered
090 * (and will have an empty bounds) if any of the array lengths are invalid
091 * or if any of the values in the faces array are out of range.
092 * 
093 * @since JavaFX 8
094 */
095public class TriangleMesh extends Mesh {
096
097    public static final int NUM_COMPONENTS_PER_POINT = 3;
098    public static final int NUM_COMPONENTS_PER_TEXCOORD = 2;
099    public static final int NUM_COMPONENTS_PER_FACE = 6;
100
101    // TODO: 3D - Need to validate the size and range of these arrays.
102    // A warning will be recorded to the logger and the mesh will have an empty
103    // bounds if the validation failed. (RT-30451)
104    // The maximum length for points (65536 * 3), texCoords (65536 * 2)
105    // or faces (65536 * 6) arrays. The values in faces must be within range.
106    // The length of points, texCoords and faces must be divisible by 3, 2 and 6 respectively.
107    private final ObservableFloatArray points = FXCollections.observableFloatArray();
108    private final ObservableFloatArray texCoords = FXCollections.observableFloatArray();
109    private final ObservableIntegerArray faces = FXCollections.observableIntegerArray();
110    private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray();
111    
112    private final Listener pointsSyncer = new Listener(points);
113    private final Listener texCoordsSyncer = new Listener(texCoords);
114    private final Listener facesSyncer = new Listener(faces);
115    private final Listener faceSmoothingGroupsSyncer = new Listener(faceSmoothingGroups);
116
117    private int refCount = 1;
118
119    private BaseBounds cachedBounds;
120
121    /**
122     * Creates a new instance of {@code TriangleMesh} class.
123     */
124    public TriangleMesh() {
125    }
126
127    /**
128     * Gets the {@code ObservableFloatArray} of points of this {@code TriangleMesh}.
129     *
130     * @return {@code ObservableFloatArray} of points where each point is
131     * represented by 3 float values x, y and z, in that order.
132     */    
133    public ObservableFloatArray getPoints() {
134        return points;
135    }
136
137    /**
138     * Gets the {@code ObservableFloatArray} of texture coordinates of this {@code TriangleMesh}.
139     *
140     * @return {@code ObservableFloatArray} array of texture coordinates
141     * where each texture coordinate is represented by 2 float values: u and v,
142     * in that order
143     */    
144    public ObservableFloatArray getTexCoords() {
145        return texCoords;
146    }
147 
148    /**
149     * Gets the {@code ObservableIntegerArray} of faces, indices into the points 
150     * and texCoords arrays, of this  {@code TriangleMesh}
151     *
152     * @return {@code ObservableIntegerArray} of faces where each face is
153     * 6 integers p0, t0, p1, t1, p3, t3, where p0, p1 and p2 are indices of 
154     * points in points {@code ObservableFloatArray} and t0, t1 and t2 are 
155     * indices of texture coordinates in texCoords {@code ObservableFloatArray}.
156     * Both indices are in terms of vertices (points or texCoords), not individual
157     * floats.
158     */    
159    public ObservableIntegerArray getFaces() {
160        return faces;
161    }
162
163    /**
164     * Gets the {@code ObservableIntegerArray} of face smoothing groups 
165     * of this {@code TriangleMesh}.
166     * Smoothing affects how a mesh is rendered but it does not effect its
167     * geometry. The face smoothing group value is used to control the smoothing
168     * between adjacent faces.
169     *
170     * <p> The face smoothing group is represented by an array of bits and up to
171     * 32 unique groups is possible; (1 << 0) to (1 << 31). The face smoothing
172     * group value can range from 0 (no smoothing group) to all 32 groups. A face
173     * can belong to zero or more smoothing groups. A face is a member of group
174     * N if bit N is set, for example, groups |= (1 << N). A value of 0 implies
175     * no smoothing group or hard edges.
176     * Smoothing is applied when adjacent pair of faces shared a smoothing group.
177     * Otherwise the faces are rendered with a hard edge between them.
178     *
179     * <p> An empty faceSmoothingGroups implies all faces in this mesh have a
180     * smoothing group value of 1.
181     *
182     * <p> Note: If faceSmoothingGroups is not empty, is size must
183     * be equal to number of faces.
184     */    
185    public ObservableIntegerArray getFaceSmoothingGroups() {
186        return faceSmoothingGroups;
187    }
188
189    @Override void setDirty(boolean value) {
190        super.setDirty(value);
191        if (!value) { // false
192            pointsSyncer.setDirty(false);
193            texCoordsSyncer.setDirty(false);
194            facesSyncer.setDirty(false);
195            faceSmoothingGroupsSyncer.setDirty(false);
196        }
197    }
198
199    int getRefCount() {
200        return refCount;
201    }
202
203    synchronized void incRef() {
204        this.refCount += 1;
205    }
206
207    synchronized void decRef() {
208        this.refCount -= 1;
209    }
210
211    private PGTriangleMesh peer;
212
213    /**
214     * @treatAsPrivate implementation detail
215     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
216     */
217    @Deprecated
218    /** The peer node created by the graphics Toolkit/Pipeline implementation */
219    PGTriangleMesh impl_getPGTriangleMesh() {
220        if (peer == null) {
221            peer = Toolkit.getToolkit().createPGTriangleMesh();
222        }
223        return peer;
224    }
225
226    @Override
227    PGTriangleMesh getPGMesh() {
228        return impl_getPGTriangleMesh();
229    }
230
231    /**
232     * @treatAsPrivate implementation detail
233     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
234     */
235    @Deprecated
236    @Override
237    void impl_updatePG() {
238        if (!isDirty()) {
239            return;
240        }
241
242        PGTriangleMesh pgTriMesh = impl_getPGTriangleMesh();
243        // sync points 
244        if (pointsSyncer.dirty) {
245            pgTriMesh.syncPoints(pointsSyncer);
246        }
247        if (texCoordsSyncer.dirty) {
248            pgTriMesh.syncTexCoords(texCoordsSyncer);
249        }
250        if (facesSyncer.dirty) {
251            pgTriMesh.syncFaces(facesSyncer);
252        }
253        if (faceSmoothingGroupsSyncer.dirty) {
254            pgTriMesh.syncFaceSmoothingGroups(faceSmoothingGroupsSyncer);
255        }
256        setDirty(false);
257    }
258
259    @Override
260    BaseBounds computeBounds(BaseBounds bounds) {
261        if (isDirty() || cachedBounds == null) {
262            cachedBounds = new BoxBounds();
263
264            final double len = points.size();
265            for (int i = 0; i < len; i += NUM_COMPONENTS_PER_POINT) {
266                cachedBounds.add(points.get(i), points.get(i + 1), points.get(i + 2));
267            }
268        }
269        return bounds.deriveWithNewBounds(cachedBounds);
270    }
271
272    /**
273     * Computes the centroid of the given triangle
274     * @param v0 vertex of the triangle
275     * @param v1 vertex of the triangle
276     * @param v2 vertex of the triangle
277     * @return the triangle centroid
278     */
279    private Point3D computeCentroid(Point3D v0, Point3D v1, Point3D v2) {
280        Point3D center = v1.midpoint(v2);
281
282        Point3D vec = center.subtract(v0);
283        return v0.add(new Point3D(vec.getX() / 3.0, vec.getY() / 3.0, vec.getZ() / 3.0));
284    }
285
286    /**
287     * Computes the centroid of the given triangle
288     * @param v0 vertex of the triangle
289     * @param v1 vertex of the triangle
290     * @param v2 vertex of the triangle
291     * @return the triangle centroid
292     */
293    private Point2D computeCentroid(Point2D v0, Point2D v1, Point2D v2) {
294        Point2D center = v1.midpoint(v2);
295
296        Point2D vec = center.subtract(v0);
297        return v0.add(new Point2D(vec.getX() / 3.0, vec.getY() / 3.0));
298    }
299
300    /**
301     * Computes intersection of a pick ray and a single triangle face.
302     *
303     * It takes pickRay, origin and dir. The latter two can be of course obtained
304     * from the pickRay, but we need them to be converted to Point3D and don't
305     * want to do that for all faces. Therefore the conversion is done just once
306     * and passed to the method for all the faces.
307     *
308     * @param pickRay pick ray
309     * @param origin pick ray's origin
310     * @param dir pick ray's direction
311     * @param faceIndex index of the face to test
312     * @param cullFace cull face of the Node (and thus the tested face)
313     * @param candidate the owner node (for the possible placement to the result)
314     * @param reportFace whether or not to report he hit face
315     * @param result the pick result to be updated if a closer intersection is found
316     * @return true if the pick ray intersects with the face (regardless of whether
317     *              the result has been updated)
318     */
319    private boolean computeIntersectsFace(
320            PickRay pickRay, Point3D origin, Point3D dir, int faceIndex,
321            CullFace cullFace, Node candidate, boolean reportFace, PickResultChooser result) {
322
323        final int v0Idx = faces.get(faceIndex) * NUM_COMPONENTS_PER_POINT;
324        final int v1Idx = faces.get(faceIndex + 2) * NUM_COMPONENTS_PER_POINT;
325        final int v2Idx = faces.get(faceIndex + 4) * NUM_COMPONENTS_PER_POINT;
326
327        final Point3D v0 = new Point3D(points.get(v0Idx), points.get(v0Idx + 1), points.get(v0Idx + 2));
328        final Point3D v1 = new Point3D(points.get(v1Idx), points.get(v1Idx + 1), points.get(v1Idx + 2));
329        final Point3D v2 = new Point3D(points.get(v2Idx), points.get(v2Idx + 1), points.get(v2Idx + 2));
330
331        final Point3D e1 = v1.subtract(v0);
332        final Point3D e2 = v2.subtract(v0);
333
334        final Point3D h = dir.crossProduct(e2);
335
336        final double a = e1.dotProduct(h);
337        if (a == 0.0) {
338            return false;
339        }
340        final double f = 1.0 / a;
341
342        final Point3D s = origin.subtract(v0);
343
344        final double u = f * (s.dotProduct(h));
345
346        if (u < 0.0 || u > 1.0) {
347            return false;
348        }
349
350        Point3D q = s.crossProduct(e1);
351        double v = f * dir.dotProduct(q);
352
353        if (v < 0.0 || u + v > 1.0) {
354            return false;
355        }
356
357        final double t = f * e2.dotProduct(q);
358
359        if (t >= pickRay.getNearClip() && t <= pickRay.getFarClip()) {
360            if (cullFace != CullFace.NONE) {
361                final Point3D normal = e1.crossProduct(e2);
362                final double nangle = normal.angle(
363                        new Point3D(-dir.getX(), -dir.getY(), -dir.getZ()));
364                if ((nangle >= 90 || cullFace != CullFace.BACK) &&
365                        (nangle <= 90 || cullFace != CullFace.FRONT)) {
366                    // hit culled face
367                    return false;
368                }
369            }
370
371            if (Double.isInfinite(t) || Double.isNaN(t)) {
372                // we've got a nonsense pick ray or triangle
373                return false;
374            }
375
376            if (result == null || !result.isCloser(t)) {
377                // it intersects, but we are not interested in the result
378                // or we already have a better (closer) result
379                // so we can omit the point and texture computation
380                return true;
381            }
382
383            Point3D point = PickResultChooser.computePoint(pickRay, t);
384
385            // Now compute texture mapping. First rotate the triangle
386            // so that we can compute in 2D
387
388            final Point3D centroid = computeCentroid(v0, v1, v2);
389            final Point3D cv0 = v0.subtract(centroid);
390            final Point3D cv1 = v1.subtract(centroid);
391            final Point3D cv2 = v2.subtract(centroid);
392
393            final Point3D ce1 = cv1.subtract(cv0);
394            final Point3D ce2 = cv2.subtract(cv0);
395            Point3D n = ce1.crossProduct(ce2);
396            if (n.getZ() < 0) {
397                n = new Point3D(-n.getX(), -n.getY(), -n.getZ());
398            }
399            final Point3D ax = n.crossProduct(Rotate.Z_AXIS);
400            final double angle = Math.atan2(ax.magnitude(), n.dotProduct(Rotate.Z_AXIS));
401
402            Rotate r = new Rotate(Math.toDegrees(angle), ax);
403            final Point3D crv0 = r.transform(cv0);
404            final Point3D crv1 = r.transform(cv1);
405            final Point3D crv2 = r.transform(cv2);
406            final Point3D rPoint = r.transform(point.subtract(centroid));
407
408            final Point2D flatV0 = new Point2D(crv0.getX(), crv0.getY());
409            final Point2D flatV1 = new Point2D(crv1.getX(), crv1.getY());
410            final Point2D flatV2 = new Point2D(crv2.getX(), crv2.getY());
411            final Point2D flatPoint = new Point2D(rPoint.getX(), rPoint.getY());
412
413            // Obtain the texture triangle
414
415            final int t0Idx = faces.get(faceIndex + 1) * NUM_COMPONENTS_PER_TEXCOORD;
416            final int t1Idx = faces.get(faceIndex + 3) * NUM_COMPONENTS_PER_TEXCOORD;
417            final int t2Idx = faces.get(faceIndex + 5) * NUM_COMPONENTS_PER_TEXCOORD;
418
419            final Point2D u0 = new Point2D(texCoords.get(t0Idx), texCoords.get(t0Idx + 1));
420            final Point2D u1 = new Point2D(texCoords.get(t1Idx), texCoords.get(t1Idx + 1));
421            final Point2D u2 = new Point2D(texCoords.get(t2Idx), texCoords.get(t2Idx + 1));
422
423            final Point2D txCentroid = computeCentroid(u0, u1, u2);
424
425            final Point2D cu0 = u0.subtract(txCentroid);
426            final Point2D cu1 = u1.subtract(txCentroid);
427            final Point2D cu2 = u2.subtract(txCentroid);
428
429            // Find the transform between the two triangles
430
431            final Affine src = new Affine(
432                    flatV0.getX(), flatV1.getX(), flatV2.getX(),
433                    flatV0.getY(), flatV1.getY(), flatV2.getY());
434            final Affine trg = new Affine(
435                    cu0.getX(), cu1.getX(), cu2.getX(),
436                    cu0.getY(), cu1.getY(), cu2.getY());
437
438            Point2D txCoords = null;
439
440            try {
441                src.invert();
442                trg.append(src);
443                txCoords = txCentroid.add(trg.transform(flatPoint));
444            } catch (NonInvertibleTransformException e) {
445                // Can't compute texture mapping, probably the coordinates
446                // don't make sense. Ignore it and return null tex coords.
447            }
448
449            result.offer(candidate, t, 
450                    reportFace ? faceIndex / NUM_COMPONENTS_PER_FACE : PickResult.FACE_UNDEFINED,
451                    point, txCoords);
452            return true;
453        }
454
455        return false;
456    }
457
458
459    /**
460     * @treatAsPrivate implementation detail
461     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
462     */
463    @Override
464    @Deprecated
465    protected boolean impl_computeIntersects(PickRay pickRay, PickResultChooser pickResult, 
466            Node candidate, CullFace cullFace, boolean reportFace) {
467
468        boolean found = false;
469        final int size = faces.size();
470
471        final Vec3d o = pickRay.getOriginNoClone();
472        final Point3D origin = new Point3D(o.x, o.y, o.z);
473
474        final Vec3d d = pickRay.getDirectionNoClone();
475        final Point3D dir = new Point3D(d.x, d.y, d.z);
476
477        for (int i = 0; i < size; i += NUM_COMPONENTS_PER_FACE) {
478            if (computeIntersectsFace(pickRay, origin, dir, i, cullFace, candidate, 
479                    reportFace, pickResult)) {
480                found = true;
481            }
482        }
483
484        return found;
485    }
486
487    private class Listener<T extends ObservableArray<T>> implements ArrayChangeListener<T>, FloatArraySyncer, IntegerArraySyncer {
488        
489        protected final T array;
490        protected boolean dirty;
491        /**
492         * Array was replaced
493         * @return true if array was replaced; false otherwise
494         */
495        protected boolean dirtyInFull;
496        protected int dirtyRangeFrom;
497        protected int dirtyRangeLength;
498
499        public Listener(T array) {
500            this.array = array;
501            array.addListener(this);
502        }
503
504        /**
505         * Adds a dirty range
506         * @param from index of the first modified element
507         * @param length length of the modified range
508         */
509        protected final void addDirtyRange(int from, int length) {
510            if (length > 0 && !dirtyInFull) {
511                markDirty();
512                if (dirtyRangeLength == 0) {
513                    dirtyRangeFrom = from;
514                    dirtyRangeLength = length;
515                } else {
516                    int fromIndex = Math.min(dirtyRangeFrom, from);
517                    int toIndex = Math.max(dirtyRangeFrom + dirtyRangeLength, from + length);
518                    dirtyRangeFrom = fromIndex;
519                    dirtyRangeLength = toIndex - fromIndex;
520                }
521            }
522        }
523
524        protected void markDirty() {
525            dirty = true;
526            TriangleMesh.this.setDirty(true);
527        }
528
529        @Override
530        public void onChanged(T observableArray, boolean sizeChanged, int from, int to) {
531            if (sizeChanged) {
532                setDirty(true);
533            } else {
534                addDirtyRange(from, to - from);
535            }
536        }
537
538        /**
539         * @param dirty if true, the whole collection is marked as dirty;
540         * if false, the whole collection is marked as not-dirty
541         */
542        public final void setDirty(boolean dirty) {
543            this.dirtyInFull = dirty;
544            if (dirty) {
545                markDirty();
546                dirtyRangeFrom = 0;
547                dirtyRangeLength = array.size();
548            } else {
549                this.dirty = false;
550                dirtyRangeFrom = dirtyRangeLength = 0;
551            }
552        }
553
554        @Override
555        public float[] syncTo(float[] array) {
556            ObservableFloatArray floatArray = (ObservableFloatArray) this.array;
557            if (dirtyInFull || array == null || array.length != floatArray.size()) {
558                return floatArray.toArray(array);
559            }
560            floatArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
561            return array;
562        }
563
564        @Override
565        public int[] syncTo(int[] array) {
566            ObservableIntegerArray intArray = (ObservableIntegerArray) this.array;
567            if (dirtyInFull || array == null || array.length != intArray.size()) {
568                return intArray.toArray(array);
569            }
570            intArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
571            return array;
572        }
573    }
574}