1 /* 2 * Copyright (c) 2013, 2016, 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.geom.BaseBounds; 29 import com.sun.javafx.geom.PickRay; 30 import com.sun.javafx.geom.Vec3d; 31 import com.sun.javafx.geom.transform.BaseTransform; 32 import com.sun.javafx.scene.DirtyBits; 33 import com.sun.javafx.scene.NodeHelper; 34 import com.sun.javafx.scene.input.PickResultChooser; 35 import com.sun.javafx.scene.shape.MeshHelper; 36 import com.sun.javafx.scene.shape.SphereHelper; 37 import com.sun.javafx.sg.prism.NGNode; 38 import com.sun.javafx.sg.prism.NGSphere; 39 import javafx.beans.property.DoubleProperty; 40 import javafx.beans.property.SimpleDoubleProperty; 41 import javafx.geometry.Point2D; 42 import javafx.geometry.Point3D; 43 import javafx.scene.Node; 44 import javafx.scene.input.PickResult; 45 import javafx.scene.transform.Rotate; 46 47 /** 48 * The {@code Sphere} class defines a 3 dimensional sphere with the specified size. 49 * A {@code Sphere} is a 3D geometry primitive created with a given radius. 50 * It is centered at the origin. 51 * 52 * @since JavaFX 8.0 53 */ 54 public class Sphere extends Shape3D { 55 static { 56 // This is used by classes in different packages to get access to 57 // private and package private methods. 58 SphereHelper.setSphereAccessor(new SphereHelper.SphereAccessor() { 59 @Override 60 public NGNode doCreatePeer(Node node) { 61 return ((Sphere) node).doCreatePeer(); 62 } 63 64 @Override 65 public void doUpdatePeer(Node node) { 66 ((Sphere) node).doUpdatePeer(); 67 } 68 69 @Override 70 public BaseBounds doComputeGeomBounds(Node node, 71 BaseBounds bounds, BaseTransform tx) { 72 return ((Sphere) node).doComputeGeomBounds(bounds, tx); 73 } 74 75 @Override 76 public boolean doComputeContains(Node node, double localX, double localY) { 77 return ((Sphere) node).doComputeContains(localX, localY); 78 } 79 80 @Override 81 public boolean doComputeIntersects(Node node, PickRay pickRay, 82 PickResultChooser pickResult) { 83 return ((Sphere) node).doComputeIntersects(pickRay, pickResult); 84 } 85 }); 86 } 87 88 static final int DEFAULT_DIVISIONS = 64; 89 static final double DEFAULT_RADIUS = 1; 90 private int divisions = DEFAULT_DIVISIONS; 91 private TriangleMesh mesh; 92 93 /** 94 * Creates a new instance of {@code Sphere} of radius of 1.0. 95 * The resolution defaults to MID_RESOLUTION divisions along sphere's axes. 96 */ 97 public Sphere() { 98 this(DEFAULT_RADIUS, DEFAULT_DIVISIONS); 99 } 100 101 /** 102 * Creates a new instance of {@code Sphere} of a given radius. 103 * The resolution defaults to MID_RESOLUTION divisions along sphere's axes. 104 * 105 * @param radius Radius 106 */ 107 public Sphere(double radius) { 108 this(radius, DEFAULT_DIVISIONS); 109 } 110 111 /** 112 * Creates a new instance of {@code Sphere} of a given radius and number 113 * of divisions. 114 * The resolution is defined in terms of number of subdivisions along the 115 * sphere's axes. More divisions lead to more finely tesselated objects. 116 * 117 * Note that divisions should be at least 1. Any value less than that will be 118 * clamped to 1. 119 * 120 * @param radius Radius 121 * @param divisions Divisions 122 */ 123 public Sphere(double radius, int divisions) { 124 SphereHelper.initHelper(this); 125 this.divisions = divisions < 1 ? 1: divisions; 126 setRadius(radius); 127 } 128 129 /** 130 * Defines the radius of the Sphere. 131 * 132 * @defaultValue 1.0 133 */ 134 private DoubleProperty radius; 135 136 public final void setRadius(double value) { 137 radiusProperty().set(value); 138 } 139 140 public final double getRadius() { 141 return radius == null ? 1 : radius.get(); 142 } 143 144 public final DoubleProperty radiusProperty() { 145 if (radius == null) { 146 radius = new SimpleDoubleProperty(Sphere.this, "radius", DEFAULT_RADIUS) { 147 @Override 148 public void invalidated() { 149 NodeHelper.markDirty(Sphere.this, DirtyBits.MESH_GEOM); 150 manager.invalidateSphereMesh(key); 151 key = 0; 152 NodeHelper.geomChanged(Sphere.this); 153 } 154 }; 155 } 156 return radius; 157 } 158 159 /** 160 * Retrieves the divisions attribute use to generate this sphere. 161 * 162 * @return the divisions attribute. 163 */ 164 public int getDivisions() { 165 return divisions; 166 } 167 168 /* 169 * Note: This method MUST only be called via its accessor method. 170 */ 171 private NGNode doCreatePeer() { 172 return new NGSphere(); 173 } 174 175 /* 176 * Note: This method MUST only be called via its accessor method. 177 */ 178 private void doUpdatePeer() { 179 if (NodeHelper.isDirty(this, DirtyBits.MESH_GEOM)) { 180 final NGSphere pgSphere = NodeHelper.getPeer(this); 181 final float r = (float) getRadius(); 182 if (r < 0) { 183 pgSphere.updateMesh(null); 184 } else { 185 if (key == 0) { 186 key = generateKey(r, divisions); 187 } 188 mesh = manager.getSphereMesh(r, divisions, key); 189 mesh.updatePG(); 190 pgSphere.updateMesh(mesh.getPGTriangleMesh()); 191 } 192 } 193 } 194 195 /* 196 * Note: This method MUST only be called via its accessor method. 197 */ 198 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { 199 final float r = (float) getRadius(); 200 201 if (r < 0) { 202 return bounds.makeEmpty(); 203 } 204 205 bounds = bounds.deriveWithNewBounds(-r, -r, -r, r, r ,r); 206 bounds = tx.transform(bounds, bounds); 207 return bounds; 208 } 209 210 /* 211 * Note: This method MUST only be called via its accessor method. 212 */ 213 private boolean doComputeContains(double localX, double localY) { 214 double r = getRadius(); 215 double n2 = localX * localX + localY * localY; 216 return n2 <= r * r; 217 } 218 219 /* 220 * Note: This method MUST only be called via its accessor method. 221 */ 222 private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult) { 223 224 final boolean exactPicking = divisions < DEFAULT_DIVISIONS && mesh != null; 225 226 final double r = getRadius(); 227 final Vec3d dir = pickRay.getDirectionNoClone(); 228 final double dirX = dir.x; 229 final double dirY = dir.y; 230 final double dirZ = dir.z; 231 final Vec3d origin = pickRay.getOriginNoClone(); 232 final double originX = origin.x; 233 final double originY = origin.y; 234 final double originZ = origin.z; 235 236 // Coeficients of a quadratic equation desribing intersection with sphere 237 final double a = dirX * dirX + dirY * dirY + dirZ * dirZ; 238 final double b = 2 * (dirX * originX + dirY * originY + dirZ * originZ); 239 final double c = originX * originX + originY * originY + originZ * originZ - r * r; 240 241 final double discriminant = b * b - 4 * a * c; 242 if (discriminant < 0) { 243 // No real roots of the equation, missed the shape 244 return false; 245 } 246 247 final double distSqrt = Math.sqrt(discriminant); 248 final double q = (b < 0) ? (-b - distSqrt) / 2.0 : (-b + distSqrt) / 2.0; 249 250 double t0 = q / a; 251 double t1 = c / q; 252 253 if (t0 > t1) { 254 final double temp = t0; 255 t0 = t1; 256 t1 = temp; 257 } 258 259 final double minDistance = pickRay.getNearClip(); 260 final double maxDistance = pickRay.getFarClip(); 261 262 if (t1 < minDistance || t0 > maxDistance) { 263 // the sphere is out of clipping planes 264 return false; 265 } 266 267 double t = t0; 268 final CullFace cullFace = getCullFace(); 269 if (t0 < minDistance || cullFace == CullFace.FRONT) { 270 if (t1 <= maxDistance && getCullFace() != CullFace.BACK) { 271 // picking the back wall 272 t = t1; 273 } else { 274 // we are inside the sphere with the back wall culled, but the 275 // exact picking still needs to be done because the front faced 276 // triangles may still be in front of us 277 if (!exactPicking) { 278 return false; 279 } 280 } 281 } 282 283 if (Double.isInfinite(t) || Double.isNaN(t)) { 284 // We've got a nonsense pick ray or sphere size. 285 return false; 286 } 287 288 if (exactPicking) { 289 return MeshHelper.computeIntersects(mesh, pickRay, pickResult, this, cullFace, false); 290 } 291 292 if (pickResult != null && pickResult.isCloser(t)) { 293 final Point3D point = PickResultChooser.computePoint(pickRay, t); 294 295 // computing texture coords 296 final Point3D proj = new Point3D(point.getX(), 0, point.getZ()); 297 final Point3D cross = proj.crossProduct(Rotate.Z_AXIS); 298 double angle = proj.angle(Rotate.Z_AXIS); 299 if (cross.getY() > 0) { 300 angle = 360 - angle; 301 } 302 Point2D txtCoords = new Point2D(1 - angle / 360, 0.5 + point.getY() / (2 * r)); 303 304 pickResult.offer(this, t, PickResult.FACE_UNDEFINED, point, txtCoords); 305 } 306 return true; 307 } 308 309 private static int correctDivisions(int div) { 310 return ((div + 3) / 4) * 4; 311 } 312 313 static TriangleMesh createMesh(int div, float r) { 314 div = correctDivisions(div); 315 316 // NOTE: still create mesh for degenerated sphere 317 final int div2 = div / 2; 318 319 final int nPoints = div * (div2 - 1) + 2; 320 final int nTPoints = (div + 1) * (div2 - 1) + div * 2; 321 final int nFaces = div * (div2 - 2) * 2 + div * 2; 322 323 final float rDiv = 1.f / div; 324 325 float points[] = new float[nPoints * 3]; 326 float tPoints[] = new float[nTPoints * 2]; 327 int faces[] = new int[nFaces * 6]; 328 329 int pPos = 0, tPos = 0; 330 331 for (int y = 0; y < div2 - 1; ++y) { 332 float va = rDiv * (y + 1 - div2 / 2) * 2 * (float) Math.PI; 333 float sin_va = (float) Math.sin(va); 334 float cos_va = (float) Math.cos(va); 335 336 float ty = 0.5f + sin_va * 0.5f; 337 for (int i = 0; i < div; ++i) { 338 double a = rDiv * i * 2 * (float) Math.PI; 339 float hSin = (float) Math.sin(a); 340 float hCos = (float) Math.cos(a); 341 points[pPos + 0] = hSin * cos_va * r; 342 points[pPos + 2] = hCos * cos_va * r; 343 points[pPos + 1] = sin_va * r; 344 tPoints[tPos + 0] = 1 - rDiv * i; 345 tPoints[tPos + 1] = ty; 346 pPos += 3; 347 tPos += 2; 348 } 349 tPoints[tPos + 0] = 0; 350 tPoints[tPos + 1] = ty; 351 tPos += 2; 352 } 353 354 points[pPos + 0] = 0; 355 points[pPos + 1] = -r; 356 points[pPos + 2] = 0; 357 points[pPos + 3] = 0; 358 points[pPos + 4] = r; 359 points[pPos + 5] = 0; 360 pPos += 6; 361 362 int pS = (div2 - 1) * div; 363 364 float textureDelta = 1.f / 256; 365 for (int i = 0; i < div; ++i) { 366 tPoints[tPos + 0] = 1.0f - rDiv * (0.5f + i); 367 tPoints[tPos + 1] = textureDelta; 368 tPos += 2; 369 } 370 371 for (int i = 0; i < div; ++i) { 372 tPoints[tPos + 0] = 1.0f - rDiv * (0.5f + i); 373 tPoints[tPos + 1] = 1 - textureDelta; 374 tPos += 2; 375 } 376 377 int fIndex = 0; 378 for (int y = 0; y < div2 - 2; ++y) { 379 for (int x = 0; x < div; ++x) { 380 int p0 = y * div + x; 381 int p1 = p0 + 1; 382 int p2 = p0 + div; 383 int p3 = p1 + div; 384 385 int t0 = p0 + y; 386 int t1 = t0 + 1; 387 int t2 = t0 + (div + 1); 388 int t3 = t1 + (div + 1); 389 390 // add p0, p1, p2 391 faces[fIndex + 0] = p0; 392 faces[fIndex + 1] = t0; 393 faces[fIndex + 2] = p1 % div == 0 ? p1 - div : p1; 394 faces[fIndex + 3] = t1; 395 faces[fIndex + 4] = p2; 396 faces[fIndex + 5] = t2; 397 fIndex += 6; 398 399 // add p3, p2, p1 400 faces[fIndex + 0] = p3 % div == 0 ? p3 - div : p3; 401 faces[fIndex + 1] = t3; 402 faces[fIndex + 2] = p2; 403 faces[fIndex + 3] = t2; 404 faces[fIndex + 4] = p1 % div == 0 ? p1 - div : p1; 405 faces[fIndex + 5] = t1; 406 fIndex += 6; 407 } 408 } 409 410 int p0 = pS; 411 int tB = (div2 - 1) * (div + 1); 412 for (int x = 0; x < div; ++x) { 413 int p2 = x, p1 = x + 1, t0 = tB + x; 414 faces[fIndex + 0] = p0; 415 faces[fIndex + 1] = t0; 416 faces[fIndex + 2] = p1 == div ? 0 : p1; 417 faces[fIndex + 3] = p1; 418 faces[fIndex + 4] = p2; 419 faces[fIndex + 5] = p2; 420 fIndex += 6; 421 } 422 423 p0 = p0 + 1; 424 tB = tB + div; 425 int pB = (div2 - 2) * div; 426 427 for (int x = 0; x < div; ++x) { 428 int p1 = pB + x, p2 = pB + x + 1, t0 = tB + x; 429 int t1 = (div2 - 2) * (div + 1) + x, t2 = t1 + 1; 430 faces[fIndex + 0] = p0; 431 faces[fIndex + 1] = t0; 432 faces[fIndex + 2] = p1; 433 faces[fIndex + 3] = t1; 434 faces[fIndex + 4] = p2 % div == 0 ? p2 - div : p2; 435 faces[fIndex + 5] = t2; 436 fIndex += 6; 437 } 438 439 TriangleMesh m = new TriangleMesh(true); 440 m.getPoints().setAll(points); 441 m.getTexCoords().setAll(tPoints); 442 m.getFaces().setAll(faces); 443 return m; 444 } 445 446 private static int generateKey(float r, int div) { 447 int hash = 5; 448 hash = 23 * hash + Float.floatToIntBits(r); 449 hash = 23 * hash + div; 450 return hash; 451 } 452 }