/* * Copyright (C) 2013-2015 F(X)yz, * Sean Phillips, Jason Pollastrini and Jose Pereda * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.fxyz.shapes.complex.cloth; import static java.lang.Math.sqrt; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.function.BiFunction; import java.util.logging.Logger; import java.util.stream.IntStream; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableFloatArray; import javafx.collections.ObservableIntegerArray; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.scene.input.PickResult; import javafx.scene.paint.Color; import javafx.scene.paint.PhongMaterial; import javafx.scene.shape.MeshView; import javafx.scene.shape.ObservableFaceArray; import javafx.scene.shape.TriangleMesh; import javafx.scene.transform.Affine; import javafx.util.Duration; import org.fxyz.geometry.Point3D; import org.fxyz.utils.FloatCollector; /** * * @author Jason Pollastrini aka jdub1581 */ public class ClothMesh extends MeshView { /** * Static Default Variables */ private static final Logger log = Logger.getLogger(ClothMesh.class.getName()); private static final int DEFAULT_DIVISIONS_X = 75; private static final int DEFAULT_DIVISIONS_Y = 35; private static final int DEFAULT_WIDTH = 600; private static final int DEFAULT_HEIGHT = 200; private static final double DEFAULT_BEND_STRENGTH = 0.85; private static final double DEFAULT_SHEAR_STRENGTH = 0.75; private static final double DEFAULT_STRETCH_STRENGTH = 0.55; private static final int DEFAULT_CONSTRAINT_ACCURACY = 8; private static final int DEFAULT_ITERATIONS = 5; private static final double DEFAULT_POINT_MASS = 1.0; //========================================================================== private final ClothTimer timer = new ClothTimer(); private TriangleMesh mesh = new TriangleMesh(); private final PhongMaterial material = new PhongMaterial(); private final List<WeightedPoint> points = new ArrayList<>(); private final Affine affine = new Affine(); private BiFunction<Integer, TriangleMesh, int[]> faceValues = (index, m) -> { if (index > ((m.getFaces().size() - 1) - m.getFaceElementSize())) { return null; } if (index > 0) { index = (index * 6); return m.getFaces().toArray(index, null, 6); } return m.getFaces().toArray(index, null, index + 6); }; private EventHandler<MouseEvent> onPressed; /** * Builds a ClothMesh with default settings */ public ClothMesh() { this( DEFAULT_DIVISIONS_X, DEFAULT_DIVISIONS_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_BEND_STRENGTH, DEFAULT_SHEAR_STRENGTH, DEFAULT_STRETCH_STRENGTH ); } /** * Builds a ClothMesh with width and height; defaults others * * @param width * @param height */ public ClothMesh(double width, double height) { this( DEFAULT_DIVISIONS_X, DEFAULT_DIVISIONS_Y, width, height, DEFAULT_BEND_STRENGTH, DEFAULT_SHEAR_STRENGTH, DEFAULT_STRETCH_STRENGTH ); } /** * Builds a ClothMesh with divsX, divsY; defaults others * * @param dx * @param dy */ public ClothMesh(int dx, int dy) { this( dx, dy, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_BEND_STRENGTH, DEFAULT_SHEAR_STRENGTH, DEFAULT_STRETCH_STRENGTH ); } /** * Builds a ClothMesh with divsX, divsY, width and height; defaults others * * @param dx * @param dy * @param width * @param height */ public ClothMesh(int dx, int dy, double width, double height) { this( dx, dy, width, height, DEFAULT_BEND_STRENGTH, DEFAULT_SHEAR_STRENGTH, DEFAULT_STRETCH_STRENGTH ); } /** * Builds a ClothMesh with divsX, divsY, width and height, stretchStrength; * defaults others * * @param dx * @param dy * @param width * @param height * @param stretch */ public ClothMesh(int dx, int dy, double width, double height, double stretch) { this( dx, dy, width, height, DEFAULT_BEND_STRENGTH, DEFAULT_SHEAR_STRENGTH, stretch ); } /** * Builds a ClothMesh using settings * * @param divsX divisions along X axis * @param divsY divisions along Y axis * @param width requested width * @param height requested height * @param bendStr strength of bend links * @param shearStr strength of shear links * @param stretchStr strength of stretch links */ public ClothMesh(int divsX, int divsY, double width, double height, double bendStr, double shearStr, double stretchStr) { assert divsX >= 4; this.setDivisionsX(divsX); assert divsY >= 4; this.setDivisionsY(divsY); this.setWidth(width); this.setHeight(height); this.setStretchStrength(stretchStr); this.setBendStrength(bendStr); this.setShearStrength(shearStr); this.getTransforms().add(affine); this.buildMesh(getDivisionsX(), getDivisionsY(), getWidth(), getHeight(), isUsingShearLinks(), isUsingBendingLinks()); this.setOnMousePressed((MouseEvent me) -> { if (me.isPrimaryButtonDown()) { PickResult pr = me.getPickResult(); if (pr.getIntersectedFace() != -1) { int[] vals = faceValues.apply(pr.getIntersectedFace(), mesh); if (me.isControlDown()) { points.get(vals[0]).setOldPosition(points.get(vals[0]).getOldPosition().add(0, 0, 25)); points.get(vals[2]).setOldPosition(points.get(vals[2]).getOldPosition().add(0, 0, 25)); points.get(vals[4]).setOldPosition(points.get(vals[4]).getOldPosition().add(0, 0, 25)); } else { points.get(vals[0]).setOldPosition(points.get(vals[0]).getOldPosition().add(0, 0, -25)); points.get(vals[2]).setOldPosition(points.get(vals[2]).getOldPosition().add(0, 0, -25)); points.get(vals[4]).setOldPosition(points.get(vals[4]).getOldPosition().add(0, 0, -25)); } } } }); } /*========================================================================== Updating Methods */ /** * */ private void updatePoints() { float[] pts = this.points.stream() .flatMapToDouble(wp -> { return wp.getPosition().getCoordinates(); }) .collect(() -> new FloatCollector(this.points.size() * 3), FloatCollector::add, FloatCollector::join) .toArray(); mesh.getPoints().setAll(pts, 0, pts.length); } /** * */ public void updateUI() { updatePoints(); } /*========================================================================== Mesh Creation *///======================================================================= /** * @param divsX number of points along X axis * @param divsY number of points along Y axis * @param width desired Width of Mesh * @param height desired Height of Mesh * @param stretch constraint elasticity / stiffness */ private void buildMesh(int divsX, int divsY, double width, double height, boolean shear, boolean bend) { float minX = (float) (-width / 2f), maxX = (float) (width / 2f), minY = (float) (-height / 2f), maxY = (float) (height / 2f); int sDivX = (divsX - 1), sDivY = (divsY - 1); double xDist = (width / divsX), yDist = (height / divsY); //build Points and TexCoords for (int Y = 0; Y <= sDivY; Y++) { float currY = (float) Y / sDivY; float fy = (1 - currY) * minY + currY * maxY; for (int X = 0; X <= sDivX; X++) { float currX = (float) X / sDivX; float fx = (1 - currX) * minX + currX * maxX; //create point: parent, mass, x, y, z WeightedPoint p = new WeightedPoint(this, getPerPointMass(), fx, fy, Math.random()); //Pin Points in place if (Y == 0 && X == 0 || (X == 0 && Y == sDivY)) { p.setAnchored(true); p.setForceAffected(false); } else { p.setForceAffected(true); } if (((Y < 5) && (X == 0)) || ((Y > sDivY - 5) && X == 0)) { p.setMass(100); } // stabilLinks if (X != 0) { p.attatchTo((points.get(points.size() - 1)), xDist, getStretchStrength()); //log.log(Level.INFO, "\nLINK-INFO\nOther Index: {0}, This Index: {1}\nLink Distance: {2}\nStiffness: {3}\n", new Object[]{(points.size() - 2), points.indexOf(p),(width / divsX), stiffness}); } if (Y != 0) { p.attatchTo((points.get((Y - 1) * (divsX) + X)), yDist, getStretchStrength()); //log.log(Level.INFO, "\nLINK-INFO\nOther Index: {0}, This Index: {1}\nLink Distance: {2}\nStiffness: {3}\n", new Object[]{((Y - 1) * (divsX) + X), points.indexOf(p),(height / divsY), stiffness}); } //add to points points.add(p); // add Point data into Mesh mesh.getPoints().addAll(p.position.x, p.position.y, p.position.z); // add texCoords mesh.getTexCoords().addAll(currX, currY); } } //shearLinks if (shear) { for (int Y = 0; Y <= sDivY; Y++) { for (int X = 0; X <= sDivX; X++) { WeightedPoint p = points.get(Y * divsX + X); // top left(xy) to right(xy + 1) if (X < (divsX - 1) && Y < (divsY - 1)) { p.attatchTo((points.get(((Y + 1) * (divsX) + (X + 1)))), sqrt((xDist * xDist) + (yDist * yDist)), getShearStrength()); } // index(xy) to left(x - 1(y + 1)) if (Y != 0 && X != (divsX - 1)) { p.attatchTo((points.get(((Y - 1) * divsX + (X + 1)))), sqrt((xDist * xDist) + (yDist * yDist)), getShearStrength()); } } } } //bendLinks if (bend) { for (int Y = 0; Y <= sDivY; Y++) { for (int X = 0; X <= sDivX; X++) { WeightedPoint p = points.get(Y * divsX + X); //skip every other if (X < (divsX - 2)) { p.attatchTo((points.get((Y * divsX + (X + 2)))), xDist * 2, getBendStrength()); } if (Y < (divsY - 2)) { p.attatchTo((points.get((Y + 2) * divsX + X)), xDist * 2, getBendStrength()); } p.setOldPosition(p.getPosition()); } } } // build faces for (int Y = 0; Y < sDivY; Y++) { for (int X = 0; X < sDivX; X++) { int p00 = Y * (sDivX + 1) + X; int p01 = p00 + 1; int p10 = p00 + (sDivX + 1); int p11 = p10 + 1; int tc00 = Y * (sDivX + 1) + X; int tc01 = tc00 + 1; int tc10 = tc00 + (sDivX + 1); int tc11 = tc10 + 1; mesh.getFaces().addAll(p00, tc00, p10, tc10, p11, tc11); mesh.getFaces().addAll(p11, tc11, p01, tc01, p00, tc00); } } //set triMesh setMesh(mesh); setMaterial(material); } /*========================================================================== Properties *///======================================================================= private final DoubleProperty width = new SimpleDoubleProperty(this, "width", DEFAULT_WIDTH) { @Override protected void invalidated() { } }; public final double getWidth() { return width.get(); } public final void setWidth(double value) { width.set(value); } public DoubleProperty widthProperty() { return width; } //========================================================================== private final DoubleProperty height = new SimpleDoubleProperty(this, "height", DEFAULT_HEIGHT) { @Override protected void invalidated() { } }; public final double getHeight() { return height.get(); } public final void setHeight(double value) { height.set(value); } public DoubleProperty heightProperty() { return height; } //========================================================================== private final IntegerProperty divisionsX = new SimpleIntegerProperty(this, "divisionsX", DEFAULT_DIVISIONS_X) { @Override protected void invalidated() { } }; public final int getDivisionsX() { return divisionsX.get(); } public final void setDivisionsX(int value) { divisionsX.set(value); } public final IntegerProperty divisionsXProperty() { return divisionsX; } //========================================================================== private final IntegerProperty divisionsY = new SimpleIntegerProperty(this, "divisionsY", DEFAULT_DIVISIONS_Y) { @Override protected void invalidated() { } }; public final int getDivisionsY() { return divisionsY.get(); } public final void setDivisionsY(int value) { divisionsY.set(value); } public final IntegerProperty divisionsYProperty() { return divisionsY; } /*========================================================================== Constraint Strengths */ private final DoubleProperty stretchStrength = new SimpleDoubleProperty(this, "stretchStrength", DEFAULT_STRETCH_STRENGTH) { @Override protected void invalidated() { } }; public final double getStretchStrength() { return stretchStrength.get(); } public final void setStretchStrength(double value) { stretchStrength.set(value); } public final DoubleProperty stretchStrengthProperty() { return stretchStrength; } //========================================================================== private final DoubleProperty shearStrength = new SimpleDoubleProperty(this, "shearStrength", DEFAULT_SHEAR_STRENGTH) { @Override protected void invalidated() { } }; public final double getShearStrength() { return shearStrength.get(); } public final void setShearStrength(double value) { shearStrength.set(value); } public final DoubleProperty shearStrengthProperty() { return shearStrength; } //========================================================================== private final DoubleProperty bendStrength = new SimpleDoubleProperty(this, "bendStrength", DEFAULT_BEND_STRENGTH) { @Override protected void invalidated() { } }; public final double getBendStrength() { return bendStrength.get(); } public final void setBendStrength(double value) { bendStrength.set(value); } public final DoubleProperty bendStrengthProperty() { return bendStrength; } /*========================================================================== Use Constraints? */ private final BooleanProperty useBendingLinks = new SimpleBooleanProperty(this, "useBendingLinks", true) { @Override protected void invalidated() { } }; public final boolean isUsingBendingLinks() { return useBendingLinks.get(); } public final void setUseBendingLinks(boolean value) { useBendingLinks.set(value); } public final BooleanProperty usingBendingLinksProperty() { return useBendingLinks; } //========================================================================== private final BooleanProperty useShearLinks = new SimpleBooleanProperty(this, "useShearLinks", true) { @Override protected void invalidated() { } }; public final boolean isUsingShearLinks() { return useShearLinks.get(); } public final void setUseShearLinks(boolean value) { useShearLinks.set(value); } public final BooleanProperty usingShearLinksProperty() { return useShearLinks; } /*========================================================================== Timer related Properties */ private final IntegerProperty constraintAccuracy = new SimpleIntegerProperty(this, "constraintAccuracy", DEFAULT_CONSTRAINT_ACCURACY); public final int getConstraintAccuracy() { return constraintAccuracy.get(); } public final void setConstraintAccuracy(int value) { constraintAccuracy.set(value); } public final IntegerProperty constraintAccuracyProperty() { return constraintAccuracy; } //========================================================================== private final IntegerProperty iterations = new SimpleIntegerProperty(this, "iterations", DEFAULT_ITERATIONS); public final int getIterations() { return iterations.get(); } public final void setIterations(int value) { iterations.set(value); } public final IntegerProperty iterationsProperty() { return iterations; } /**========================================================================= * Starts the Cloth Simulation */ public final void startSimulation() { if (!timer.isRunning()) { timer.start(); } } /** * Pauses the Cloth Simulation */ public final void pauseSimulation() { timer.pause(); } /** * Stops the Cloth Simulation */ public final void stopSimulation() { timer.cancel(); } /*========================================================================== * Material Delagates * *///======================================================================== public final void setDiffuseColor(Color value) { material.setDiffuseColor(value); } public final Color getDiffuseColor() { return material.getDiffuseColor(); } public final ObjectProperty<Color> diffuseColorProperty() { return material.diffuseColorProperty(); } public final void setSpecularColor(Color value) { material.setSpecularColor(value); } public final Color getSpecularColor() { return material.getSpecularColor(); } public final ObjectProperty<Color> specularColorProperty() { return material.specularColorProperty(); } public final void setSpecularPower(double value) { material.setSpecularPower(value); } public final double getSpecularPower() { return material.getSpecularPower(); } public final DoubleProperty specularPowerProperty() { return material.specularPowerProperty(); } public final void setDiffuseMap(Image value) { material.setDiffuseMap(value); } public final Image getDiffuseMap() { return material.getDiffuseMap(); } public final ObjectProperty<Image> diffuseMapProperty() { return material.diffuseMapProperty(); } public final void setSpecularMap(Image value) { material.setSpecularMap(value); } public final Image getSpecularMap() { return material.getSpecularMap(); } public final ObjectProperty<Image> specularMapProperty() { return material.specularMapProperty(); } public final void setBumpMap(Image value) { material.setBumpMap(value); } public final Image getBumpMap() { return material.getBumpMap(); } public final ObjectProperty<Image> bumpMapProperty() { return material.bumpMapProperty(); } public final void setSelfIlluminationMap(Image value) { material.setSelfIlluminationMap(value); } public final Image getSelfIlluminationMap() { return material.getSelfIlluminationMap(); } public final ObjectProperty<Image> selfIlluminationMapProperty() { return material.selfIlluminationMapProperty(); } /*========================================================================== * TriangleMesh Data */ public final ObservableFloatArray getPoints() { return mesh.getPoints(); } public final ObservableFloatArray getTexCoords() { return mesh.getTexCoords(); } public final ObservableFaceArray getFaces() { return mesh.getFaces(); } public final ObservableIntegerArray getFaceSmoothingGroups() { return mesh.getFaceSmoothingGroups(); } /*========================================================================== * Point Properties */ private final DoubleProperty perPointMass = new SimpleDoubleProperty(this, "perPointMass", DEFAULT_POINT_MASS); public double getPerPointMass() { return perPointMass.get(); } public void setPerPointMass(double value) { perPointMass.set(value); } public void setPointsMass(int index, double m){ points.get(index).setMass(m); } public DoubleProperty perPointMassProperty() { return perPointMass; } /*========================================================================== Force for Points */ private final ObjectProperty<Point3D> accumulatedForces = new SimpleObjectProperty<>(this, "accumulatedForces"); public Point3D getAccumulatedForces() { return accumulatedForces.get(); } public void addForce(Point3D f){ setAccumulatedForces(getAccumulatedForces().add(f)); } public void setAccumulatedForces(Point3D value) { accumulatedForces.set(value); } public ObjectProperty<Point3D> accumulatedForcesProperty() { return accumulatedForces; } /*========================================================================== Points List */ protected final List<WeightedPoint> getPointList() { return points; } //End ClothMesh============================================================= /** * ************************************************************************* * ClothTimer is a simple timer class for updating points * * @author Jason * Pollastrini aka jdub1581 * ************************************************************************* */ /** * Timer to handle Cloth updates */ private class ClothTimer extends ScheduledService<Void> { private final long ONE_NANO = 1000000000L; private final double ONE_NANO_INV = 1f / 1000000000L; private long startTime, previousTime; private double deltaTime; private final double fixedDeltaTime = 0.16; private int leftOverDeltaTime, timeStepAmt; private final NanoThreadFactory tf; private boolean paused; public ClothTimer() { super(); this.setPeriod(Duration.millis(16)); this.tf = new NanoThreadFactory(); this.setExecutor(Executors.newSingleThreadExecutor(tf)); } /** * @return elapsed time as a double */ public double getTimeAsSeconds() { return getTime() * ONE_NANO_INV; } /** * * @return elapsed time as a long */ public long getTime() { return System.nanoTime() - startTime; } /** * * @return one nano second */ private long getOneSecondAsNano() { return ONE_NANO; } /** * * @return deltaTime */ public double getDeltaTime() { return deltaTime; } /** * * @return updates Timers clock values */ private void updateTimer() { deltaTime = (getTime() - previousTime) * (10.0f / ONE_NANO); previousTime = getTime(); timeStepAmt = (int) ((deltaTime + leftOverDeltaTime) / fixedDeltaTime); timeStepAmt = Math.min(timeStepAmt, 5); leftOverDeltaTime = (int) (deltaTime - (timeStepAmt * fixedDeltaTime)); } @Override protected Task<Void> createTask() { return new Task<Void>() { @Override protected Void call() throws Exception { updateTimer(); IntStream.range(0, getIterations()).forEach(i->{}); points.parallelStream().filter(p->{return points.indexOf(p) % (getDivisionsX() - 1) == 0;}).forEach(p -> { p.applyForce(new Point3D(5,-1,1)); }); for (int i = 0; i < getConstraintAccuracy(); i++) { points.parallelStream().forEach(WeightedPoint::solveConstraints); } points.parallelStream().forEach(p -> { p.applyForce(new Point3D(4.8f,1,-1)); p.updatePhysics(deltaTime, 1); }); return null; } }; } @Override protected void failed() { getException().printStackTrace(System.err); } @Override protected void succeeded() { super.succeeded(); updateUI(); } @Override protected void cancelled() { super.cancelled(); reset(); } @Override public void start() { if (isRunning()) { return; } super.start(); if (startTime <= 0) { startTime = System.nanoTime(); } } protected void pause() { paused = true; if (isRunning()) { if (cancel()) { cancelled(); } } } @Override public void reset() { super.reset(); if (!paused) { startTime = System.nanoTime(); previousTime = getTime(); } } @Override public String toString() { return "ClothTimer{" + "startTime=" + startTime + ", previousTime=" + previousTime + ", deltaTime=" + deltaTime + ", fixedDeltaTime=" + fixedDeltaTime + ", leftOverDeltaTime=" + leftOverDeltaTime + ", timeStepAmt=" + timeStepAmt + '}'; } /*========================================================================== */ private class NanoThreadFactory implements ThreadFactory { public NanoThreadFactory() { } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "ClothTimerThread"); t.setDaemon(true); return t; } } }//End ClothTimer=========================================================== }