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 }