1 /* 2 * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.shape; 27 28 import com.sun.javafx.scene.shape.ObservableFaceArrayImpl; 29 import com.sun.javafx.collections.FloatArraySyncer; 30 import com.sun.javafx.collections.IntegerArraySyncer; 31 import com.sun.javafx.geom.BaseBounds; 32 import com.sun.javafx.geom.BoxBounds; 33 import com.sun.javafx.geom.PickRay; 34 import com.sun.javafx.geom.Vec3d; 35 import com.sun.javafx.scene.input.PickResultChooser; 36 import com.sun.javafx.scene.shape.TriangleMeshHelper; 37 import com.sun.javafx.sg.prism.NGTriangleMesh; 38 import javafx.beans.property.ObjectProperty; 39 import javafx.beans.property.SimpleObjectProperty; 40 import javafx.collections.ArrayChangeListener; 41 import javafx.collections.FXCollections; 42 import javafx.collections.ObservableArray; 43 import javafx.collections.ObservableFloatArray; 44 import javafx.collections.ObservableIntegerArray; 45 import javafx.geometry.Point2D; 46 import javafx.geometry.Point3D; 47 import javafx.scene.Node; 48 import javafx.scene.input.PickResult; 49 import javafx.scene.transform.Affine; 50 import javafx.scene.transform.NonInvertibleTransformException; 51 import javafx.scene.transform.Rotate; 52 import com.sun.javafx.logging.PlatformLogger; 53 54 /** 55 * Defines a 3D triangle mesh that consists of its associated {@code VertexFormat} 56 * and a set of separate arrays of vertex components such as points, normals, 57 * texture coordinates, and an array of faces that define the individual triangles 58 * of the mesh. 59 *<p> 60 * Note that the term point, as used in the method names and method 61 * descriptions, actually refers to a 3D point (x, y, z) in space 62 * representing the position of a single vertex. The term points (plural) is 63 * used to indicate sets of 3D points for multiple vertices. 64 * Similarly, the term normal is used to indicate a 3D vector (nx, ny, nz) in space 65 * representing the direction of a single vertex. The term normals (plural) is 66 * used to indicate sets of 3D vectors for multiple vertices. 67 * The term texCoord is used to indicate a single pair of 2D texture 68 * coordinates (u, v) for a single vertex, while the term texCoords (plural) is used 69 * to indicate sets of texture coordinates for multiple vertices. 70 * Lastly, the term face is used to indicate 3 sets of interleaving points, 71 * normals (optional, depending on the associated VertexFormat) 72 * and texture coordinates that together represent the geometric topology of a 73 * single triangle, while the term faces (plural) is used to indicate sets of 74 * triangles (each represent by a face). 75 * <p> 76 * For example, the faces with {@code VertexFormat.POINT_TEXCOORD} that represent 77 * a single textured rectangle, using 2 triangles, have the following data order: [ 78 * <p> 79 * p0, t0, p1, t1, p3, t3, // First triangle of a textured rectangle 80 * <p> 81 * p1, t1, p2, t2, p3, t3 // Second triangle of a textured rectangle 82 * <p> 83 * ] 84 * <p> 85 * whereas the faces with {@code VertexFormat.POINT_NORMAL_TEXCOORD} that represent 86 * a single textured rectangle, using 2 triangles, have the following data order: [ 87 * <p> 88 * p0, n0, t0, p1, n1, t1, p3, n3, t3, // First triangle of a textured rectangle 89 * <p> 90 * p1, n1, t1, p2, n2, t2, p3, n3, t3 // Second triangle of a textured rectangle 91 * <p> 92 * ] 93 * <p> 94 * where p0, p1, p2 and p3 are indices into the points array, n0, n1, n2 and n3 95 * are indices into the normals array, and t0, t1, t2 and t3 are indices into 96 * the texCoords array. 97 * 98 * <p> 99 * A triangle has a front and back face. The winding order of a triangle's vertices 100 * determines which side is the front face. JavaFX chooses the counter-clockwise 101 * (or right-hand rule) winding order as the front face. By default, only the 102 * front face of a triangle is rendered. See {@code CullFace} for more 103 * information. 104 * 105 * <p> 106 * The length of {@code points}, {@code normals}, and {@code texCoords} must be 107 * divisible by 3, 3, and 2 respectively. The length of {@code faces} must be 108 * divisible by 6 if it is of {@code VertexFormat.POINT_TEXCOORD} else it must 109 * be divisible by 9 if it is of {@code VertexFormat.POINT_NORMAL_TEXCOORD}. 110 * The values in the faces array must be within the range of the number of vertices 111 * in the points array (0 to points.length / 3 - 1) for the point indices, within 112 * the range of the number of vertices in the normals array 113 * (0 to normals.length / 3 - 1) for the normal indices, and within the range of 114 * the number of the vertices in the texCoords array (0 to texCoords.length / 2 - 1) 115 * for the texture coordinate indices. 116 * 117 * <p> A warning will be recorded to the logger and the mesh will not be rendered 118 * (and will have an empty bounds) if any of the array lengths are invalid 119 * or if any of the values in the faces array are out of range. 120 * 121 * @since JavaFX 8.0 122 */ 123 public class TriangleMesh extends Mesh { 124 static { 125 TriangleMeshHelper.setTriangleMeshAccessor(new TriangleMeshHelper.TriangleMeshAccessor() { 126 @Override 127 public boolean doComputeIntersects(Mesh mesh, PickRay pickRay, 128 PickResultChooser pickResult, Node candidate, CullFace cullFace, 129 boolean reportFace) { 130 return ((TriangleMesh) mesh).doComputeIntersects(pickRay, 131 pickResult, candidate, cullFace, reportFace); 132 } 133 }); 134 } 135 136 private final ObservableFloatArray points = FXCollections.observableFloatArray(); 137 private final ObservableFloatArray normals = FXCollections.observableFloatArray(); 138 private final ObservableFloatArray texCoords = FXCollections.observableFloatArray(); 139 private final ObservableFaceArray faces = new ObservableFaceArrayImpl(); 140 private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray(); 141 142 private final Listener pointsSyncer = new Listener(points); 143 private final Listener normalsSyncer = new Listener(normals); 144 private final Listener texCoordsSyncer = new Listener(texCoords); 145 private final Listener facesSyncer = new Listener(faces); 146 private final Listener faceSmoothingGroupsSyncer = new Listener(faceSmoothingGroups); 147 private final boolean isPredefinedShape; 148 private boolean isValidDirty = true; 149 private boolean isPointsValid, isNormalsValid, isTexCoordsValid, isFacesValid, isFaceSmoothingGroupValid; 150 private int refCount = 1; 151 152 private BaseBounds cachedBounds; 153 154 /** 155 * Creates a new instance of {@code TriangleMesh} class with the default 156 * {@code VertexFormat.POINT_TEXCOORD} format type. 157 */ 158 public TriangleMesh() { 159 this(false); 160 TriangleMeshHelper.initHelper(this); 161 } 162 163 /** 164 * Creates a new instance of {@code TriangleMesh} class with the specified 165 * {@code VertexFormat}. 166 * 167 * @param vertexFormat specifies the vertex format type. 168 * 169 * @since JavaFX 8u40 170 */ 171 public TriangleMesh(VertexFormat vertexFormat) { 172 this(false); 173 this.setVertexFormat(vertexFormat); 174 TriangleMeshHelper.initHelper(this); 175 } 176 177 TriangleMesh(boolean isPredefinedShape) { 178 this.isPredefinedShape = isPredefinedShape; 179 if (isPredefinedShape) { 180 isPointsValid = true; 181 isNormalsValid = true; 182 isTexCoordsValid = true; 183 isFacesValid = true; 184 isFaceSmoothingGroupValid = true; 185 } else { 186 isPointsValid = false; 187 isNormalsValid = false; 188 isTexCoordsValid = false; 189 isFacesValid = false; 190 isFaceSmoothingGroupValid = false; 191 } 192 TriangleMeshHelper.initHelper(this); 193 } 194 195 /** 196 * Specifies the vertex format of this {@code TriangleMesh}, one of 197 * {@code VertexFormat.POINT_TEXCOORD} or {@code VertexFormat.POINT_NORMAL_TEXCOORD}. 198 * 199 * @defaultValue VertexFormat.POINT_TEXCOORD 200 * 201 * @since JavaFX 8u40 202 */ 203 private ObjectProperty<VertexFormat> vertexFormat; 204 205 public final void setVertexFormat(VertexFormat value) { 206 vertexFormatProperty().set(value); 207 } 208 209 public final VertexFormat getVertexFormat() { 210 return vertexFormat == null ? VertexFormat.POINT_TEXCOORD : vertexFormat.get(); 211 } 212 213 public final ObjectProperty<VertexFormat> vertexFormatProperty() { 214 if (vertexFormat == null) { 215 vertexFormat = new SimpleObjectProperty<VertexFormat>(TriangleMesh.this, "vertexFormat") { 216 217 @Override 218 protected void invalidated() { 219 setDirty(true); 220 // Need to mark faces and faceSmoothingGroups dirty too. 221 facesSyncer.setDirty(true); 222 faceSmoothingGroupsSyncer.setDirty(true); 223 } 224 }; 225 } 226 return vertexFormat; 227 } 228 229 /** 230 * Returns the number of elements that represents a point. 231 * 232 * @return number of elements 233 */ 234 public final int getPointElementSize() { 235 return getVertexFormat().getPointElementSize(); 236 } 237 238 /** 239 * Returns the number of elements that represents a normal. 240 * 241 * @return number of elements 242 * 243 * @since JavaFX 8u40 244 */ 245 public final int getNormalElementSize() { 246 return getVertexFormat().getNormalElementSize(); 247 } 248 249 /** 250 * Returns the number of elements that represents a texture coordinates. 251 * 252 * @return number of elements 253 */ 254 public final int getTexCoordElementSize() { 255 return getVertexFormat().getTexCoordElementSize(); 256 } 257 258 /** 259 * Returns the number of elements that represents a face. 260 * 261 * @return number of elements 262 */ 263 public final int getFaceElementSize() { 264 return getVertexFormat().getVertexIndexSize() * 3; 265 } 266 267 /** 268 * Gets the {@code points} array of this {@code TriangleMesh}. 269 * 270 * @return {@code points} array where each point is 271 * represented by 3 float values x, y and z, in that order. 272 */ 273 public final ObservableFloatArray getPoints() { 274 return points; 275 } 276 277 /** 278 * Gets the {@code normals} array of this {@code TriangleMesh}. 279 * 280 * @return {@code normals} array where each normal is 281 * represented by 3 float values nx, ny and nz, in that order. 282 * 283 * @since JavaFX 8u40 284 */ 285 public final ObservableFloatArray getNormals() { 286 return normals; 287 } 288 289 /** 290 * Gets the {@code texCoords} array of this {@code TriangleMesh}. 291 * The coordinates are proportional, so texture's top-left corner 292 * is at [0, 0] and bottom-right corner is at [1, 1]. 293 * 294 * @return {@code texCoord} array where each texture coordinate is represented 295 * by 2 float values: u and v, in that order. 296 */ 297 public final ObservableFloatArray getTexCoords() { 298 return texCoords; 299 } 300 301 /** 302 * Gets the {@code faces} array, indices into the {@code points}, 303 * {@code normals} (optional, if it is a {@code VertexFormat.POINT_NORMAL_TEXCOORD} 304 * mesh) and {@code texCoords} arrays, of this {@code TriangleMesh}. All 305 * indices are in terms of elements in to the points, normals or texCoords 306 * arrays not individual floats. 307 * 308 * @return {@code faces} array where each face is of 309 * 3 * {@code VertexFormat.getVertexIndexSize()} integers. 310 */ 311 public final ObservableFaceArray getFaces() { 312 return faces; 313 } 314 315 /** 316 * Gets the {@code faceSmoothingGroups} array of this {@code TriangleMesh}. 317 * Smoothing affects how a mesh is rendered but it does not effect its 318 * geometry. The face smoothing group value is used to control the smoothing 319 * between adjacent faces. 320 * 321 * <p> 322 * {@literal The face smoothing group value is represented by an array of bits and up to 323 * 32 unique groups is possible; (1 << 0) to (1 << 31). The face smoothing 324 * group value can range from 0 (no smoothing group) to all 32 groups. A face 325 * can belong to zero or more smoothing groups. A face is a member of group 326 * N if bit N is set, for example, groups |= (1 << N). A value of 0 implies 327 * no smoothing group or hard edges.} 328 * Smoothing is applied when adjacent pair of faces shared a smoothing group. 329 * Otherwise the faces are rendered with a hard edge between them. 330 * 331 * <p> An empty faceSmoothingGroups implies all faces in this mesh have a 332 * smoothing group value of 1. 333 * 334 * <p> If faceSmoothingGroups is not empty, its size must 335 * be equal to number of faces. 336 * 337 * <p> This faceSmoothingGroups has no effect on its {@code TriangleMesh} if 338 * it is of {@code VertexFormat.POINT_NORMAL_TEXCOORD} format. 339 * @return the {@code faceSmoothingGroups} array of this {@code TriangleMesh} 340 */ 341 public final ObservableIntegerArray getFaceSmoothingGroups() { 342 return faceSmoothingGroups; 343 } 344 345 @Override void setDirty(boolean value) { 346 super.setDirty(value); 347 if (!value) { // false 348 pointsSyncer.setDirty(false); 349 normalsSyncer.setDirty(false); 350 texCoordsSyncer.setDirty(false); 351 facesSyncer.setDirty(false); 352 faceSmoothingGroupsSyncer.setDirty(false); 353 } 354 } 355 356 int getRefCount() { 357 return refCount; 358 } 359 360 synchronized void incRef() { 361 this.refCount += 1; 362 } 363 364 synchronized void decRef() { 365 this.refCount -= 1; 366 } 367 368 private NGTriangleMesh peer; 369 370 /* The peer node created by the graphics Toolkit/Pipeline implementation */ 371 NGTriangleMesh getPGTriangleMesh() { 372 if (peer == null) { 373 peer = new NGTriangleMesh(); 374 } 375 return peer; 376 } 377 378 @Override 379 NGTriangleMesh getPGMesh() { 380 return getPGTriangleMesh(); 381 } 382 383 private boolean validatePoints() { 384 if (points.size() == 0) { // Valid but meaningless for picking or rendering. 385 return false; 386 } 387 388 if ((points.size() % getVertexFormat().getPointElementSize()) != 0) { 389 String logname = TriangleMesh.class.getName(); 390 PlatformLogger.getLogger(logname).warning("points.size() has " 391 + "to be divisible by getPointElementSize(). It is to" 392 + " store multiple x, y, and z coordinates of this mesh"); 393 return false; 394 } 395 return true; 396 } 397 398 private boolean validateNormals() { 399 // Only validate normals if vertex format has normal component 400 if (getVertexFormat() != VertexFormat.POINT_NORMAL_TEXCOORD) return true; 401 402 if (normals.size() == 0) { // Valid but meaningless for picking or rendering. 403 return false; 404 } 405 406 if ((normals.size() % getVertexFormat().getNormalElementSize()) != 0) { 407 String logname = TriangleMesh.class.getName(); 408 PlatformLogger.getLogger(logname).warning("normals.size() has " 409 + "to be divisible by getNormalElementSize(). It is to" 410 + " store multiple nx, ny, and nz coordinates of this mesh"); 411 return false; 412 } 413 return true; 414 } 415 416 private boolean validateTexCoords() { 417 if (texCoords.size() == 0) { // Valid but meaningless for picking or rendering. 418 return false; 419 } 420 421 if ((texCoords.size() % getVertexFormat().getTexCoordElementSize()) != 0) { 422 String logname = TriangleMesh.class.getName(); 423 PlatformLogger.getLogger(logname).warning("texCoords.size() " 424 + "has to be divisible by getTexCoordElementSize()." 425 + " It is to store multiple u and v texture coordinates" 426 + " of this mesh"); 427 return false; 428 } 429 return true; 430 } 431 432 private boolean validateFaces() { 433 if (faces.size() == 0) { // Valid but meaningless for picking or rendering. 434 return false; 435 } 436 437 String logname = TriangleMesh.class.getName(); 438 if ((faces.size() % getFaceElementSize()) != 0) { 439 PlatformLogger.getLogger(logname).warning("faces.size() has " 440 + "to be divisible by getFaceElementSize()."); 441 return false; 442 } 443 444 if (getVertexFormat() == VertexFormat.POINT_TEXCOORD) { 445 int nVerts = points.size() / getVertexFormat().getPointElementSize(); 446 int nTVerts = texCoords.size() / getVertexFormat().getTexCoordElementSize(); 447 for (int i = 0; i < faces.size(); i++) { 448 if (i % 2 == 0 && (faces.get(i) >= nVerts || faces.get(i) < 0) 449 || (i % 2 != 0 && (faces.get(i) >= nTVerts || faces.get(i) < 0))) { 450 PlatformLogger.getLogger(logname).warning("The values in the " 451 + "faces array must be within the range of the number " 452 + "of vertices in the points array (0 to points.length / 3 - 1) " 453 + "for the point indices and within the range of the " 454 + "number of the vertices in the texCoords array (0 to " 455 + "texCoords.length / 2 - 1) for the texture coordinate indices."); 456 return false; 457 } 458 } 459 } else if (getVertexFormat() == VertexFormat.POINT_NORMAL_TEXCOORD) { 460 int nVerts = points.size() / getVertexFormat().getPointElementSize(); 461 int nNVerts = normals.size() / getVertexFormat().getNormalElementSize(); 462 int nTVerts = texCoords.size() / getVertexFormat().getTexCoordElementSize(); 463 for (int i = 0; i < faces.size(); i+=3) { 464 if ((faces.get(i) >= nVerts || faces.get(i) < 0) 465 || (faces.get(i + 1) >= nNVerts || faces.get(i + 1) < 0) 466 || (faces.get(i + 2) >= nTVerts || faces.get(i + 2) < 0)) { 467 PlatformLogger.getLogger(logname).warning("The values in the " 468 + "faces array must be within the range of the number " 469 + "of vertices in the points array (0 to points.length / 3 - 1) " 470 + "for the point indices, and within the range of the " 471 + "number of the vertices in the normals array (0 to " 472 + "normals.length / 3 - 1) for the normals indices, and " 473 + "number of the vertices in the texCoords array (0 to " 474 + "texCoords.length / 2 - 1) for the texture coordinate indices."); 475 return false; 476 } 477 } 478 } else { 479 PlatformLogger.getLogger(logname).warning("Unsupported VertexFormat: " + getVertexFormat().toString()); 480 return false; 481 } 482 return true; 483 } 484 485 private boolean validateFaceSmoothingGroups() { 486 if (faceSmoothingGroups.size() != 0 487 && faceSmoothingGroups.size() != (faces.size() / getFaceElementSize())) { 488 String logname = TriangleMesh.class.getName(); 489 PlatformLogger.getLogger(logname).warning("faceSmoothingGroups.size()" 490 + " has to equal to number of faces."); 491 return false; 492 } 493 return true; 494 } 495 496 private boolean validate() { 497 if (isPredefinedShape) { 498 return true; 499 } 500 501 if (isValidDirty) { 502 if (pointsSyncer.dirtyInFull) { 503 isPointsValid = validatePoints(); 504 } 505 if (normalsSyncer.dirtyInFull) { 506 isNormalsValid = validateNormals(); 507 } 508 if (texCoordsSyncer.dirtyInFull) { 509 isTexCoordsValid = validateTexCoords(); 510 } 511 if (facesSyncer.dirty || pointsSyncer.dirtyInFull 512 || normalsSyncer.dirtyInFull || texCoordsSyncer.dirtyInFull) { 513 isFacesValid = isPointsValid && isNormalsValid 514 && isTexCoordsValid && validateFaces(); 515 } 516 if (faceSmoothingGroupsSyncer.dirtyInFull || facesSyncer.dirtyInFull) { 517 isFaceSmoothingGroupValid = isFacesValid && validateFaceSmoothingGroups(); 518 } 519 isValidDirty = false; 520 } 521 return isPointsValid && isNormalsValid && isTexCoordsValid 522 && isFaceSmoothingGroupValid && isFacesValid; 523 } 524 525 @Override 526 void updatePG() { 527 if (!isDirty()) { 528 return; 529 } 530 531 final NGTriangleMesh pgTriMesh = getPGTriangleMesh(); 532 if (validate()) { 533 pgTriMesh.setUserDefinedNormals(getVertexFormat() == VertexFormat.POINT_NORMAL_TEXCOORD); 534 pgTriMesh.syncPoints(pointsSyncer); 535 pgTriMesh.syncNormals(normalsSyncer); 536 pgTriMesh.syncTexCoords(texCoordsSyncer); 537 pgTriMesh.syncFaces(facesSyncer); 538 pgTriMesh.syncFaceSmoothingGroups(faceSmoothingGroupsSyncer); 539 } else { 540 pgTriMesh.setUserDefinedNormals(false); 541 pgTriMesh.syncPoints(null); 542 pgTriMesh.syncNormals(null); 543 pgTriMesh.syncTexCoords(null); 544 pgTriMesh.syncFaces(null); 545 pgTriMesh.syncFaceSmoothingGroups(null); 546 } 547 setDirty(false); 548 } 549 550 @Override 551 BaseBounds computeBounds(BaseBounds bounds) { 552 if (isDirty() || cachedBounds == null) { 553 cachedBounds = new BoxBounds(); 554 if (validate()) { 555 final int len = points.size(); 556 final int pointElementSize = getVertexFormat().getPointElementSize(); 557 for (int i = 0; i < len; i += pointElementSize) { 558 cachedBounds.add(points.get(i), points.get(i + 1), points.get(i + 2)); 559 } 560 } 561 } 562 return bounds.deriveWithNewBounds(cachedBounds); 563 } 564 565 /** 566 * Computes the centroid of the given triangle 567 * @param v0x x coord of first vertex of the triangle 568 * @param v0y y coord of first vertex of the triangle 569 * @param v0z z coord of first vertex of the triangle 570 * @param v1x x coord of second vertex of the triangle 571 * @param v1y y coord of second vertex of the triangle 572 * @param v1z z coord of second vertex of the triangle 573 * @param v2x x coord of third vertex of the triangle 574 * @param v2y y coord of third vertex of the triangle 575 * @param v2z z coord of third vertex of the triangle 576 * @return the triangle centroid 577 */ 578 private Point3D computeCentroid( 579 double v0x, double v0y, double v0z, 580 double v1x, double v1y, double v1z, 581 double v2x, double v2y, double v2z) { 582 583 // Point3D center = v1.midpoint(v2); 584 // Point3D vec = center.subtract(v0); 585 // return v0.add(new Point3D(vec.getX() / 3.0, vec.getY() / 3.0, vec.getZ() / 3.0)); 586 587 return new Point3D( 588 v0x + (v2x + (v1x - v2x) / 2.0 - v0x) / 3.0, 589 v0y + (v2y + (v1y - v2y) / 2.0 - v0y) / 3.0, 590 v0z + (v2z + (v1z - v2z) / 2.0 - v0z) / 3.0); 591 } 592 593 /** 594 * Computes the centroid of the given triangle 595 * @param v0 vertex of the triangle 596 * @param v1 vertex of the triangle 597 * @param v2 vertex of the triangle 598 * @return the triangle centroid 599 */ 600 private Point2D computeCentroid(Point2D v0, Point2D v1, Point2D v2) { 601 Point2D center = v1.midpoint(v2); 602 603 Point2D vec = center.subtract(v0); 604 return v0.add(new Point2D(vec.getX() / 3.0, vec.getY() / 3.0)); 605 } 606 607 /** 608 * Computes intersection of a pick ray and a single triangle face. 609 * 610 * It takes pickRay, origin and dir. The latter two can be of course obtained 611 * from the pickRay, but we need them to be converted to Point3D and don't 612 * want to do that for all faces. Therefore the conversion is done just once 613 * and passed to the method for all the faces. 614 * 615 * @param pickRay pick ray 616 * @param origin pick ray's origin 617 * @param dir pick ray's direction 618 * @param faceIndex index of the face to test 619 * @param cullFace cull face of the Node (and thus the tested face) 620 * @param candidate the owner node (for the possible placement to the result) 621 * @param reportFace whether or not to report he hit face 622 * @param result the pick result to be updated if a closer intersection is found 623 * @return true if the pick ray intersects with the face (regardless of whether 624 * the result has been updated) 625 */ 626 private boolean computeIntersectsFace( 627 PickRay pickRay, Vec3d origin, Vec3d dir, int faceIndex, 628 CullFace cullFace, Node candidate, boolean reportFace, PickResultChooser result) {//, BoxBounds rayBounds) { 629 630 // This computation was naturally done by Point3D and its operations, 631 // but it needs a lot of points and there is often a lot of triangles 632 // so it is vital for performance to use only primitive variables 633 // and do the computing manually. 634 635 int vertexIndexSize = getVertexFormat().getVertexIndexSize(); 636 int pointElementSize = getVertexFormat().getPointElementSize(); 637 final int v0Idx = faces.get(faceIndex) * pointElementSize; 638 final int v1Idx = faces.get(faceIndex + vertexIndexSize) * pointElementSize; 639 final int v2Idx = faces.get(faceIndex + (2 * vertexIndexSize)) * pointElementSize; 640 641 final float v0x = points.get(v0Idx); 642 final float v0y = points.get(v0Idx + 1); 643 final float v0z = points.get(v0Idx + 2); 644 final float v1x = points.get(v1Idx); 645 final float v1y = points.get(v1Idx + 1); 646 final float v1z = points.get(v1Idx + 2); 647 final float v2x = points.get(v2Idx); 648 final float v2y = points.get(v2Idx + 1); 649 final float v2z = points.get(v2Idx + 2); 650 651 // e1 = v1.subtract(v0) 652 final float e1x = v1x - v0x; 653 final float e1y = v1y - v0y; 654 final float e1z = v1z - v0z; 655 // e2 = v2.subtract(v0) 656 final float e2x = v2x - v0x; 657 final float e2y = v2y - v0y; 658 final float e2z = v2z - v0z; 659 660 // h = dir.crossProduct(e2) 661 final double hx = dir.y * e2z - dir.z * e2y; 662 final double hy = dir.z * e2x - dir.x * e2z; 663 final double hz = dir.x * e2y - dir.y * e2x; 664 665 // a = e1.dotProduct(h) 666 final double a = e1x * hx + e1y * hy + e1z * hz; 667 if (a == 0.0) { 668 return false; 669 } 670 final double f = 1.0 / a; 671 672 // s = origin.subtract(v0) 673 final double sx = origin.x - v0x; 674 final double sy = origin.y - v0y; 675 final double sz = origin.z - v0z; 676 677 // u = f * (s.dotProduct(h)) 678 final double u = f * (sx * hx + sy * hy + sz * hz); 679 680 if (u < 0.0 || u > 1.0) { 681 return false; 682 } 683 684 // q = s.crossProduct(e1) 685 final double qx = sy * e1z - sz * e1y; 686 final double qy = sz * e1x - sx * e1z; 687 final double qz = sx * e1y - sy * e1x; 688 689 // v = f * dir.dotProduct(q) 690 double v = f * (dir.x * qx + dir.y * qy + dir.z * qz); 691 692 if (v < 0.0 || u + v > 1.0) { 693 return false; 694 } 695 696 // t = f * e2.dotProduct(q) 697 final double t = f * (e2x * qx + e2y * qy + e2z * qz); 698 699 if (t >= pickRay.getNearClip() && t <= pickRay.getFarClip()) { 700 // This branch is entered only for hit triangles (not so often), 701 // so we can get smoothly back to the nice code using Point3Ds. 702 703 if (cullFace != CullFace.NONE) { 704 // normal = e1.crossProduct(e2) 705 final Point3D normal = new Point3D( 706 e1y * e2z - e1z * e2y, 707 e1z * e2x - e1x * e2z, 708 e1x * e2y - e1y * e2x); 709 710 final double nangle = normal.angle( 711 new Point3D(-dir.x, -dir.y, -dir.z)); 712 if ((nangle >= 90 || cullFace != CullFace.BACK) && 713 (nangle <= 90 || cullFace != CullFace.FRONT)) { 714 // hit culled face 715 return false; 716 } 717 } 718 719 if (Double.isInfinite(t) || Double.isNaN(t)) { 720 // we've got a nonsense pick ray or triangle 721 return false; 722 } 723 724 if (result == null || !result.isCloser(t)) { 725 // it intersects, but we are not interested in the result 726 // or we already have a better (closer) result 727 // so we can omit the point and texture computation 728 return true; 729 } 730 731 Point3D point = PickResultChooser.computePoint(pickRay, t); 732 733 // Now compute texture mapping. First rotate the triangle 734 // so that we can compute in 2D 735 736 // centroid = computeCentroid(v0, v1, v2); 737 final Point3D centroid = computeCentroid( 738 v0x, v0y, v0z, 739 v1x, v1y, v1z, 740 v2x, v2y, v2z); 741 742 // cv0 = v0.subtract(centroid) 743 final Point3D cv0 = new Point3D( 744 v0x - centroid.getX(), 745 v0y - centroid.getY(), 746 v0z - centroid.getZ()); 747 // cv1 = v1.subtract(centroid) 748 final Point3D cv1 = new Point3D( 749 v1x - centroid.getX(), 750 v1y - centroid.getY(), 751 v1z - centroid.getZ()); 752 // cv2 = v2.subtract(centroid) 753 final Point3D cv2 = new Point3D( 754 v2x - centroid.getX(), 755 v2y - centroid.getY(), 756 v2z - centroid.getZ()); 757 758 final Point3D ce1 = cv1.subtract(cv0); 759 final Point3D ce2 = cv2.subtract(cv0); 760 Point3D n = ce1.crossProduct(ce2); 761 if (n.getZ() < 0) { 762 n = new Point3D(-n.getX(), -n.getY(), -n.getZ()); 763 } 764 final Point3D ax = n.crossProduct(Rotate.Z_AXIS); 765 final double angle = Math.atan2(ax.magnitude(), n.dotProduct(Rotate.Z_AXIS)); 766 767 Rotate r = new Rotate(Math.toDegrees(angle), ax); 768 final Point3D crv0 = r.transform(cv0); 769 final Point3D crv1 = r.transform(cv1); 770 final Point3D crv2 = r.transform(cv2); 771 final Point3D rPoint = r.transform(point.subtract(centroid)); 772 773 final Point2D flatV0 = new Point2D(crv0.getX(), crv0.getY()); 774 final Point2D flatV1 = new Point2D(crv1.getX(), crv1.getY()); 775 final Point2D flatV2 = new Point2D(crv2.getX(), crv2.getY()); 776 final Point2D flatPoint = new Point2D(rPoint.getX(), rPoint.getY()); 777 778 // Obtain the texture triangle 779 int texCoordElementSize = getVertexFormat().getTexCoordElementSize(); 780 int texCoordOffset = getVertexFormat().getTexCoordIndexOffset(); 781 final int t0Idx = faces.get(faceIndex + texCoordOffset) * texCoordElementSize; 782 final int t1Idx = faces.get(faceIndex + vertexIndexSize + texCoordOffset) * texCoordElementSize; 783 final int t2Idx = faces.get(faceIndex + (vertexIndexSize * 2) + texCoordOffset) * texCoordElementSize; 784 785 final Point2D u0 = new Point2D(texCoords.get(t0Idx), texCoords.get(t0Idx + 1)); 786 final Point2D u1 = new Point2D(texCoords.get(t1Idx), texCoords.get(t1Idx + 1)); 787 final Point2D u2 = new Point2D(texCoords.get(t2Idx), texCoords.get(t2Idx + 1)); 788 789 final Point2D txCentroid = computeCentroid(u0, u1, u2); 790 791 final Point2D cu0 = u0.subtract(txCentroid); 792 final Point2D cu1 = u1.subtract(txCentroid); 793 final Point2D cu2 = u2.subtract(txCentroid); 794 795 // Find the transform between the two triangles 796 797 final Affine src = new Affine( 798 flatV0.getX(), flatV1.getX(), flatV2.getX(), 799 flatV0.getY(), flatV1.getY(), flatV2.getY()); 800 final Affine trg = new Affine( 801 cu0.getX(), cu1.getX(), cu2.getX(), 802 cu0.getY(), cu1.getY(), cu2.getY()); 803 804 Point2D txCoords = null; 805 806 try { 807 src.invert(); 808 trg.append(src); 809 txCoords = txCentroid.add(trg.transform(flatPoint)); 810 } catch (NonInvertibleTransformException e) { 811 // Can't compute texture mapping, probably the coordinates 812 // don't make sense. Ignore it and return null tex coords. 813 } 814 815 result.offer(candidate, t, 816 reportFace ? faceIndex / getFaceElementSize() : PickResult.FACE_UNDEFINED, 817 point, txCoords); 818 return true; 819 } 820 821 return false; 822 } 823 824 /* 825 * Note: This method MUST only be called via its accessor method. 826 */ 827 private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult, 828 Node candidate, CullFace cullFace, boolean reportFace) { 829 830 boolean found = false; 831 if (validate()) { 832 final int size = faces.size(); 833 834 final Vec3d o = pickRay.getOriginNoClone(); 835 836 final Vec3d d = pickRay.getDirectionNoClone(); 837 838 for (int i = 0; i < size; i += getFaceElementSize()) { 839 if (computeIntersectsFace(pickRay, o, d, i, cullFace, candidate, 840 reportFace, pickResult)) { 841 found = true; 842 } 843 } 844 } 845 return found; 846 } 847 848 private class Listener<T extends ObservableArray<T>> implements ArrayChangeListener<T>, FloatArraySyncer, IntegerArraySyncer { 849 850 protected final T array; 851 protected boolean dirty = true; 852 /** 853 * Array was replaced 854 * @return true if array was replaced; false otherwise 855 */ 856 protected boolean dirtyInFull = true; 857 protected int dirtyRangeFrom; 858 protected int dirtyRangeLength; 859 860 public Listener(T array) { 861 this.array = array; 862 array.addListener(this); 863 } 864 865 /** 866 * Adds a dirty range 867 * @param from index of the first modified element 868 * @param length length of the modified range 869 */ 870 protected final void addDirtyRange(int from, int length) { 871 if (length > 0 && !dirtyInFull) { 872 markDirty(); 873 if (dirtyRangeLength == 0) { 874 dirtyRangeFrom = from; 875 dirtyRangeLength = length; 876 } else { 877 int fromIndex = Math.min(dirtyRangeFrom, from); 878 int toIndex = Math.max(dirtyRangeFrom + dirtyRangeLength, from + length); 879 dirtyRangeFrom = fromIndex; 880 dirtyRangeLength = toIndex - fromIndex; 881 } 882 } 883 } 884 885 protected void markDirty() { 886 dirty = true; 887 TriangleMesh.this.setDirty(true); 888 } 889 890 @Override 891 public void onChanged(T observableArray, boolean sizeChanged, int from, int to) { 892 if (sizeChanged) { 893 setDirty(true); 894 } else { 895 addDirtyRange(from, to - from); 896 } 897 isValidDirty = true; 898 } 899 900 /** 901 * @param dirty if true, the whole collection is marked as dirty; 902 * if false, the whole collection is marked as not-dirty 903 */ 904 public final void setDirty(boolean dirty) { 905 this.dirtyInFull = dirty; 906 if (dirty) { 907 markDirty(); 908 dirtyRangeFrom = 0; 909 dirtyRangeLength = array.size(); 910 } else { 911 this.dirty = false; 912 dirtyRangeFrom = dirtyRangeLength = 0; 913 } 914 } 915 916 @Override 917 public float[] syncTo(float[] array, int[] fromAndLengthIndices) { 918 assert ((fromAndLengthIndices != null) && (fromAndLengthIndices.length == 2)); 919 ObservableFloatArray floatArray = (ObservableFloatArray) this.array; 920 if (dirtyInFull || array == null || array.length != floatArray.size()) { 921 // Always allocate a new array when size changes 922 fromAndLengthIndices[0] = 0; 923 fromAndLengthIndices[1] = floatArray.size(); 924 return floatArray.toArray(null); 925 } 926 fromAndLengthIndices[0] = dirtyRangeFrom; 927 fromAndLengthIndices[1] = dirtyRangeLength; 928 floatArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength); 929 return array; 930 } 931 932 @Override 933 public int[] syncTo(int[] array, int[] fromAndLengthIndices) { 934 assert ((fromAndLengthIndices != null) && (fromAndLengthIndices.length == 2)); 935 ObservableIntegerArray intArray = (ObservableIntegerArray) this.array; 936 if (dirtyInFull || array == null || array.length != intArray.size()) { 937 fromAndLengthIndices[0] = 0; 938 fromAndLengthIndices[1] = intArray.size(); 939 // Always allocate a new array when size changes 940 return intArray.toArray(null); 941 } 942 fromAndLengthIndices[0] = dirtyRangeFrom; 943 fromAndLengthIndices[1] = dirtyRangeLength; 944 intArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength); 945 return array; 946 } 947 } 948 }