/** * FrustumMesh.java * * Copyright (c) 2013-2016, F(X)yz * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of F(X)yz, any associated website, nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL F(X)yz BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.fxyz3d.shapes.primitives; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.DoubleStream; import java.util.stream.IntStream; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Point2D; import javafx.scene.DepthTest; import javafx.scene.shape.CullFace; import javafx.scene.shape.DrawMode; import javafx.scene.shape.TriangleMesh; import javafx.scene.transform.Affine; import javafx.scene.transform.NonInvertibleTransformException; import javafx.scene.transform.Rotate; import javafx.scene.transform.Transform; import javafx.scene.transform.Translate; import org.fxyz3d.geometry.Point3D; import org.fxyz3d.shapes.primitives.helper.TriangleMeshHelper; import org.fxyz3d.shapes.primitives.helper.TriangleMeshHelper.SectionType; import org.fxyz3d.geometry.Face3; import org.fxyz3d.collections.FloatCollector; /** * * @author José Pereda Llamas * Created on 22-dic-2014 - 21:51:51 */ public class FrustumMesh extends TexturedMesh { private final static int DEFAULT_DIVISIONS = 20; private final static double DEFAULT_MAJOR_RADIUS = 1; private final static double DEFAULT_MINOR_RADIUS = 0; private final static double DEFAULT_HEIGHT = 10; private final static int DEFAULT_LEVEL = 1; public FrustumMesh(){ this(DEFAULT_MAJOR_RADIUS, DEFAULT_MINOR_RADIUS, DEFAULT_HEIGHT, DEFAULT_LEVEL,null,null); } public FrustumMesh(double majorRadius, double minorRadius, double height){ this(majorRadius, minorRadius, height, DEFAULT_LEVEL,null,null); } public FrustumMesh(double majorRadius, double minorRadius, double height, int level){ this(majorRadius,minorRadius, height,level,null,null); } public FrustumMesh(double majorRadius, double minorRadius, double height, int level, Point3D pIni, Point3D pEnd){ setAxisOrigin(pIni==null?new Point3D(0,(float)height/2f,0):pIni); setAxisEnd(pEnd==null?new Point3D(0,-(float)height/2f,0):pEnd); setMajorRadius(majorRadius); setMinorRadius(minorRadius); setLevel(level); updateMesh(); setCullFace(CullFace.BACK); setDrawMode(DrawMode.FILL); setDepthTest(DepthTest.ENABLE); } private final DoubleProperty majorRadius = new SimpleDoubleProperty(DEFAULT_MAJOR_RADIUS){ @Override protected void invalidated() { if(mesh!=null){ updateMesh(); } } }; public double getMajorRadius() { return majorRadius.get(); } public final void setMajorRadius(double value) { majorRadius.set(value); } public DoubleProperty majorRadiusProperty() { return majorRadius; } private final DoubleProperty minorRadius = new SimpleDoubleProperty(DEFAULT_MINOR_RADIUS){ @Override protected void invalidated() { if(mesh!=null){ updateMesh(); } } }; public double getMinorRadius() { return minorRadius.get(); } public final void setMinorRadius(double value) { minorRadius.set(value); } public DoubleProperty minorRadiusProperty() { return minorRadius; } private final DoubleProperty height = new SimpleDoubleProperty(DEFAULT_HEIGHT){ @Override protected void invalidated() { if(mesh!=null){ updateMesh(); } } }; public double getHeight() { return height.get(); } public final void setHeight(double value) { height.set(value); } public DoubleProperty heightProperty() { return height; } private final IntegerProperty level = new SimpleIntegerProperty(DEFAULT_LEVEL){ @Override protected void invalidated() { if(mesh!=null){ updateMesh(); } } }; public final int getLevel() { return level.get(); } public final void setLevel(int value) { level.set(value); } public final IntegerProperty levelProperty() { return level; } private final ObjectProperty<Point3D> axisOrigin = new SimpleObjectProperty<Point3D>(){ @Override protected void invalidated() { if(getAxisOrigin()!=null && getAxisEnd()!=null){ setHeight(getAxisEnd().substract(getAxisOrigin()).magnitude()); } } }; public Point3D getAxisOrigin() { return axisOrigin.get(); } public final void setAxisOrigin(Point3D value) { axisOrigin.set(value); } public ObjectProperty axisOriginProperty() { return axisOrigin; } private final ObjectProperty<Point3D> axisEnd = new SimpleObjectProperty<Point3D>(){ @Override protected void invalidated() { if(getAxisOrigin()!=null && getAxisEnd()!=null){ setHeight(getAxisEnd().substract(getAxisOrigin()).magnitude()); } } }; public Point3D getAxisEnd() { return axisEnd.get(); } public final void setAxisEnd(Point3D value) { axisEnd.set(value); } public ObjectProperty axisEndProperty() { return axisEnd; } @Override protected final void updateMesh() { setMesh(null); mesh=createFrustum((float)getMajorRadius(), (float)getMinorRadius(), (float)getHeight(), getLevel()); setMesh(mesh); } private int numVertices, numTexCoords, numFaces; private float[] points0, texCoord0; private int[] faces0; private List<Point2D> texCoord1; /* cylinder mesh is generated on (0,h/2,0) -> (0,-h/2,0) local coordinates With Transform a we transform vertices coordinates to generate directly the cyilinder from pIni to pEnd in global coordinates: pIni == a.transform(0,h/2,0) pEnd == a.transform(0,-h/2,0) */ private Transform a = new Affine(); private TriangleMesh createFrustum(float majorRadius, float minorRadius, float height, int level){ TriangleMesh m0=null; if(level>0){ m0 = createFrustum(majorRadius, minorRadius, height, level-1); } if(level==0){ a = new Affine(); int div=DEFAULT_DIVISIONS>3?DEFAULT_DIVISIONS:3; if(getSectionType()!=TriangleMeshHelper.SectionType.CIRCLE){ div=getSectionType().getSides()*((int)(div/getSectionType().getSides())+1); } if(getAxisOrigin()!=null && getAxisEnd()!=null){ Point3D dir=getAxisEnd().substract(getAxisOrigin()).crossProduct(new Point3D(0,-1,0)); double angle=Math.acos(getAxisEnd().substract(getAxisOrigin()).normalize().dotProduct(new Point3D(0,-1,0))); a=a.createConcatenation(new Translate(getAxisOrigin().x, getAxisOrigin().y-height/2d, getAxisOrigin().z)) .createConcatenation(new Rotate(-Math.toDegrees(angle), 0d,height/2d,0d, new javafx.geometry.Point3D(dir.x,-dir.y,dir.z))); } int nPoints=2*div+2; float rBase=majorRadius; float rTop=minorRadius; float h=height; final float[] baseVertices = new float[nPoints*3]; // base at y=h/2 for(int i=0; i<div; i++){ double ang=i*2d*Math.PI/div; double pol = polygonalSection(ang); Point3D ta = transform(rBase*pol*Math.cos(ang),h/2,rBase*pol*Math.sin(ang)); baseVertices[3*i]=ta.x; baseVertices[3*i+1]=ta.y; baseVertices[3*i+2]=ta.z; } // top at y=-h/2 for(int i=div; i<2*div; i++){ double ang=i*2d*Math.PI/div; double pol = polygonalSection(ang); Point3D ta = transform(rTop*pol*Math.cos(ang),-h/2,rTop*pol*Math.sin(ang)); baseVertices[3*i]=ta.x; baseVertices[3*i+1]=ta.y; baseVertices[3*i+2]=ta.z; } Point3D ta = transform(0,h/2,0); baseVertices[6*div]=ta.x; baseVertices[6*div+1]=ta.y; baseVertices[6*div+2]=ta.z; ta = transform(0,-h/2,0); baseVertices[6*div+3]=ta.x; baseVertices[6*div+4]=ta.y; baseVertices[6*div+5]=ta.z; int nTextCoords=div*4+6; float rectBase=(float)polygonalSize(rBase); float rectTop=(float)polygonalSize(rTop); float L=(float)(rBase+2d*Math.PI*rBase); float H=2f*(rBase+rTop)+h; final float[] baseTexCoords = new float[nTextCoords*2]; // u right ,v up for(int i=0; i<=div; i++){ baseTexCoords[2*i]=(float)(rBase+i*rectBase/div)/L; baseTexCoords[2*i+1]=(float)(2f*rTop+h)/H; } for(int i=0; i<=div; i++){ baseTexCoords[2*div+2*i+2]=(float)(rBase+i*rectTop/div)/L; baseTexCoords[2*div+2*i+3]=(float)(2f*rTop)/H; } for(int i=0; i<=div; i++){ double ang=i*2d*Math.PI/div; double pol = polygonalSection(ang); baseTexCoords[4*div+2*i+4]=(float)(rBase+rBase*pol*Math.sin(ang))/L; baseTexCoords[4*div+2*i+5]=(float)(2f*rTop+rBase+h-rBase*pol*Math.cos(ang))/H; } for(int i=0; i<=div; i++){ double ang=i*2d*Math.PI/div; double pol = polygonalSection(ang); baseTexCoords[6*div+2*i+6]=(float)(rBase+rTop*pol*Math.sin(ang))/L; baseTexCoords[6*div+2*i+7]=(float)(rTop+rTop*pol*Math.cos(ang))/H; } baseTexCoords[8*div+8]=rBase/L; baseTexCoords[8*div+9]=(2f*rTop+rBase+h)/H; baseTexCoords[8*div+10]=rBase/L; baseTexCoords[8*div+11]=rTop/H; int nFaces=div*4; final int[] baseTexture = new int[nFaces*3]; final int[] baseFaces = new int[nFaces*3]; for(int i=0; i<div; i++){ int p1=i+1; int p2=i+div; int p3=i+div+1; baseFaces[6*i]=i; baseFaces[6*i+1]=p1==div?0:p1; baseFaces[6*i+2]=p2; baseFaces[6*i+3]=p3%div==0?p3-div:p3; baseFaces[6*i+4]=p2; baseFaces[6*i+5]=p1==div?0:p1; baseTexture[6*i]=i; baseTexture[6*i+1]=p1; baseTexture[6*i+2]=p2+1; baseTexture[6*i+3]=p3+1; baseTexture[6*i+4]=p2+1; baseTexture[6*i+5]=p1; } for(int i=0; i<div; i++){ int p1=div*2; int p2=i+1; baseFaces[6*div+3*i]=i; baseFaces[6*div+3*i+1]=p1; baseFaces[6*div+3*i+2]=p2==div?0:p2; baseTexture[6*div+3*i]=(div+1)*2+i; baseTexture[6*div+3*i+1]=(div+1)*4; baseTexture[6*div+3*i+2]=(div+1)*2+i+1; } for(int i=0; i<div; i++){ int p1=div*2+1; int p2=i+1+div; baseFaces[9*div+3*i]=i+div; baseFaces[9*div+3*i+1]=p2%div==0?p2-div:p2; baseFaces[9*div+3*i+2]=p1; baseTexture[9*div+3*i]=(div+1)*3+i; baseTexture[9*div+3*i+1]=(div+1)*3+i+1; baseTexture[9*div+3*i+2]=(div+1)*4+1; } points0 = baseVertices; numVertices=baseVertices.length/3; texCoord0 = baseTexCoords; numTexCoords=baseTexCoords.length/2; faces0 = IntStream.range(0, baseFaces.length/3) .mapToObj(i->IntStream.of(baseFaces[3*i], baseTexture[3*i], baseFaces[3*i+1], baseTexture[3*i+1], baseFaces[3*i+2], baseTexture[3*i+2])) .flatMapToInt(i->i).toArray(); numFaces=baseFaces.length/3; } else if(m0!=null) { points0=new float[numVertices*m0.getPointElementSize()]; m0.getPoints().toArray(points0); } final float h=height; List<Point3D> points1 = IntStream.range(0, numVertices) .mapToObj(i -> { Point3D p=new Point3D(points0[3*i], points0[3*i+1], points0[3*i+2]); // f = h of local cylinder from 0 on top (ini) to 1 on bottom (end) p.f=(h/2-unTransform(p).y)/h; return p; }) .collect(Collectors.toList()); if(level>0 && m0!=null){ texCoord0=new float[numTexCoords*m0.getTexCoordElementSize()]; m0.getTexCoords().toArray(texCoord0); } texCoord1 = IntStream.range(0, numTexCoords) .mapToObj(i -> new Point2D(texCoord0[2*i], texCoord0[2*i+1])) .collect(Collectors.toList()); if(level>0 && m0!=null){ faces0=new int[numFaces*m0.getFaceElementSize()]; m0.getFaces().toArray(faces0); } List<Face3> faces1 = IntStream.range(0, numFaces) .mapToObj(i -> new Face3(faces0[6*i], faces0[6*i+2], faces0[6*i+4])) .collect(Collectors.toList()); index.set(points1.size()); map.clear(); listVertices.clear(); listFaces.clear(); listVertices.addAll(points1); faces1.forEach(face->{ int v1=face.p0; int v2=face.p1; int v3=face.p2; if(level>0){ int a = getMiddle(v1,points1.get(v1),v2,points1.get(v2)); int b = getMiddle(v2,points1.get(v2),v3,points1.get(v3)); int c = getMiddle(v3,points1.get(v3),v1,points1.get(v1)); listFaces.add(new Face3(v1,a,c)); listFaces.add(new Face3(v2,b,a)); listFaces.add(new Face3(v3,c,b)); listFaces.add(new Face3(a,b,c)); } else { listFaces.add(new Face3(v1,v2,v3)); } }); map.clear(); numVertices=listVertices.size(); numFaces=listFaces.size(); List<Face3> textures1; if(level==0){ textures1= IntStream.range(0, faces0.length/6) .mapToObj(i -> new Face3(faces0[6*i+1], faces0[6*i+3], faces0[6*i+5])) .collect(Collectors.toList()); } else { textures1 = listTextures.stream().map(t->t).collect(Collectors.toList()); } index.set(texCoord1.size()); listTextures.clear(); AtomicInteger kk=new AtomicInteger(); textures1.forEach(face->{ int v1=face.p0; int v2=face.p1; int v3=face.p2; if(level>0){ int a = getMiddle(v1,texCoord1.get(v1),v2,texCoord1.get(v2)); int b = getMiddle(v2,texCoord1.get(v2),v3,texCoord1.get(v3)); int c = getMiddle(v3,texCoord1.get(v3),v1,texCoord1.get(v1)); listTextures.add(new Face3(v1,a,c)); listTextures.add(new Face3(v2,b,a)); listTextures.add(new Face3(v3,c,b)); listTextures.add(new Face3(a,b,c)); } else { listTextures.add(new Face3(v1,v2,v3)); } }); map.clear(); texCoord0=texCoord1.stream().flatMapToDouble(p->DoubleStream.of(p.getX(),p.getY())) .collect(()->new FloatCollector(texCoord1.size()*2), FloatCollector::add, FloatCollector::join).toArray(); numTexCoords=texCoord0.length/2; textureCoords=texCoord0; if(level==getLevel()){ areaMesh.setWidth(majorRadius+2f*Math.PI*majorRadius); areaMesh.setHeight(height+2f*(minorRadius+majorRadius)); smoothingGroups=IntStream.range(0,listFaces.size()).map(i->{ if(getSectionType()!=TriangleMeshHelper.SectionType.CIRCLE){ return 0; } if(i<listFaces.size()/2){ return 1; } else if(i<3*listFaces.size()/4){ return 2; } return 4; }).toArray(); } return createMesh(); } private Point3D transform(Point3D p){ javafx.geometry.Point3D ta = a.transform(p.x,p.y,p.z); return new Point3D((float)ta.getX(), (float)ta.getY(), (float)ta.getZ()); } private Point3D transform(double x, double y, double z){ javafx.geometry.Point3D ta = a.transform(x,y,z); return new Point3D((float)ta.getX(), (float)ta.getY(), (float)ta.getZ()); } public Point3D unTransform(Point3D p){ try { javafx.geometry.Point3D ta = a.inverseTransform(p.x,p.y,p.z); return new Point3D((float)ta.getX(), (float)ta.getY(), (float)ta.getZ()); } catch (NonInvertibleTransformException ex) { System.out.println("p not invertible "+p); } return p; } private final AtomicInteger index = new AtomicInteger(); private final HashMap<String, Integer> map = new HashMap<>(); private int getMiddle(int v1, Point3D p1, int v2, Point3D p2){ String key = ""+Math.min(v1,v2)+"_"+Math.max(v1,v2); if(map.get(key)!=null){ return map.get(key); } Point3D p3 = p1.add(p2).multiply(0.5f); if(getSectionType().equals(SectionType.CIRCLE)){ if(inCircle(p1) && inCircle(p2)){ Point3D p4 = unTransform(p3); float fact=(float)(radius(p4.y)/Math.sqrt(p4.x*p4.x+p4.z*p4.z)); p3=transform(fact*p4.x,p4.y,fact*p4.z); if(!inCircle(p3)){ System.out.println("p3: "+p3); } } } // f = h of local cylinder from 0 on top (ini) to 1 on bottom (end) p3.f=(float)((height.get()/2d-unTransform(p3).y)/height.get()); listVertices.add(p3); map.put(key,index.get()); return index.getAndIncrement(); } private double radius(double y){ return majorRadius.get()+(minorRadius.get()-majorRadius.get())*(height.get()/2d-y)/height.get(); } private boolean inCircle(Point3D p){ Point3D p2=unTransform(p); return p2.x*p2.x+p2.z*p2.z>0.99*Math.pow(radius(p2.y),2); } private int getMiddle(int v1, Point2D p1, int v2, Point2D p2){ String key = ""+Math.min(v1,v2)+"_"+Math.max(v1,v2); if(map.get(key)!=null){ return map.get(key); } texCoord1.add(p1.add(p2).multiply(0.5f)); map.put(key,index.get()); return index.getAndIncrement(); } }