/* ========================
* JSynoptic : a free Synoptic editor
* ========================
*
* Project Info: http://jsynoptic.sourceforge.net/index.html
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Foundation;
* either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307, USA.
*
* (C) Copyright 2001-2004, by :
* Corporate:
* EADS Corporate Research Center
* Individual:
* Nicolas Brodu
*
* $Id: XYZResultNodeJava3D.java,v 1.2 2005/09/02 12:18:02 ogor Exp $
*
* Changes
* -------
* 01-Jun-2004 : Creation date (NB);
*
*/
package examples.syn3d.plugin.java3d;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.util.List;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.Bounds;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.Group;
import javax.media.j3d.J3DBuffer;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TriangleArray;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3f;
import syn3d.base.ActiveNode;
import syn3d.nodes.java3d.SceneNodeJava3D;
import syn3d.nodes.java3d.ShapeNodeJava3D;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.picking.PickIntersection;
import com.sun.j3d.utils.picking.PickResult;
import com.sun.j3d.utils.picking.PickTool;
/**
* This specialisation of XYZResult node creates 3D objects corresponding
* to the file content, using Java3D.
*
* It also handles picking, as an example how to do this very useful
* feature with Java3D.
*
* @see XYZResultNode
*/
public class XYZResultNodeJava3D extends XYZResultNode {
/** This node creates a tranform group so as to easily center and scale the shape */
protected TransformGroup transformGroup;
/** Used internally, the file name is displayed in the tree */
protected String fileName;
/**
* The file may contain multiple variables per point (temperature, voltage...), but
* only one can be displayed at a given time. This gives the current variable number,
* it should be greater than 3 (since the first variables in the file are supposed to
* be X, Y, Z).
*/
protected int currentVariable;
/**
* This specialisation of XYZResultNode creates a Java3D tranform group
* to hold the shape defined by the data in the file.
* A transformation will then be applied to scale and center the shape.
*/
public XYZResultNodeJava3D(ActiveNode parent) {
super(parent);
transformGroup = new TransformGroup();
transformGroup.setUserData(this);
transformGroup.setPickable(true);
transformGroup.setCapability(TransformGroup.ALLOW_BOUNDS_READ);
transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
transformGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
transformGroup.setBoundsAutoCompute(true);
if (parent.get3DObject() instanceof Group) {
SceneNodeJava3D.detach(this);
// Parent should have allowed children extend in Syn3D, no need to detach the scene
((Group)parent.get3DObject()).addChild(transformGroup);
SceneNodeJava3D.attach(this);
}
}
/* (non-Javadoc)
* @see syn3d.base.ActiveNode#get3DObject()
*/
public Object get3DObject() {
return transformGroup;
}
/* (non-Javadoc)
* @see syn3d.base.ActiveNode#remove()
*/
public void remove() {
SceneNodeJava3D.removeChildFromParentGroup(transformGroup);
}
// delegate action to children if a file is already loaded
public List getActions() {
// delegate action to children if a file is already loaded
if (children.size()>0) {
return ((ActiveNode)children.get(0)).getActions();
}
return super.getActions();
}
// delegate action to children if a file is already loaded
public void doAction(Object action) {
if (children.size()>0) {
((ActiveNode)children.get(0)).doAction(action);
return;
}
super.doAction(action);
}
// Overload parent loading to add the creation of a shape using Java3D
protected void load(File file) throws IOException {
// parent method loads the data from the file
super.load(file);
// Create the geometry
// Use NIO buffers in Java3D to increase performances
// => JVM buffers needs to be copied twice each rendering (JVM => memory => video RAM)
// => NIO buffers are already in main memory
// In addition to improving the performance, this also dramatically decreases the
// lag effect of loading a scene larger than the video RAM. Indeed, when using JVM
// memory, the sanction is immediate : the scene becomes quite impossible to manipulate.
// With NIO buffers, there is a progressive degradation of performances when the scene
// doesn't fit entirely in the 3D card RAM.
DoubleBuffer nioCoords = ByteBuffer.allocateDirect(triangles.length*3 * 8).order(ByteOrder.nativeOrder()).asDoubleBuffer();
FloatBuffer nioColors = ByteBuffer.allocateDirect(triangles.length*3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
FloatBuffer nioNormals = ByteBuffer.allocateDirect(triangles.length * 3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
// Set up the coordinates loaded by the parent method into the
// NIO buffers.
// This also expand the geometry. The file is described in indexed mode : triangles
// are defined by indexing points.
// Java3D also has an indexed mode, and using it would be immediate.
// Unfortunately, when using an indexed mode, the triangle vertices are shared by definition.
// It is thus not possible to change the values for the vertices for only one triangle
// For example, when highlighting a triangle, it is necessary to modify the colors of
// this triangle only, not its neighbors.
// On the other hand, if picking/highlighting was done on vertices only, then modifying
// all triangles using this vertex could be a good idea.
// In this example, a triangle is picked, and the closest vertex is also shown, but
// highlighted only in this triangle. This is not that complicated, but shows potential
// problems. One should thus be able to easily adapt this example to simpler cases.
for (int i=0; i<triangles.length; i+=3) {
int pt = triangles[i];
nioCoords.put(data[0][pt]);
nioCoords.put(data[1][pt]);
nioCoords.put(data[2][pt]);
pt = triangles[i+1];
nioCoords.put(data[0][pt]);
nioCoords.put(data[1][pt]);
nioCoords.put(data[2][pt]);
pt = triangles[i+2];
nioCoords.put(data[0][pt]);
nioCoords.put(data[1][pt]);
nioCoords.put(data[2][pt]);
}
// Create a triangle array using NIO.
// Unfortunately, Sun picking utilities don't handle NIO very well, thus
// we need to overload a method
TriangleArray ta = new TriangleArray(triangles.length, GeometryArray.BY_REFERENCE|GeometryArray.USE_NIO_BUFFER|GeometryArray.COORDINATES|GeometryArray.NORMALS|GeometryArray.COLOR_3) {
public double[] getCoordRefDouble() {
// When picking, tests have shown that NIO buffer copy has no noticeable impact
// on performance. So, it is actually better to copy the array each time the picking is
// done than to hog memory with a permanent copy.
// It is also still better to copy the buffer only when picking rather than at each rendering
double[] array = new double[getVertexCount()*3];
DoubleBuffer db = (DoubleBuffer)super.getCoordRefBuffer().getBuffer();
db.rewind();
db.get(array); // optimized get
return array;
}
};
ta.setCoordRefBuffer(new J3DBuffer(nioCoords));
// Use Java3D utilities to compute normals
GeometryInfo gi = new GeometryInfo(GeometryInfo.TRIANGLE_ARRAY);
double[] coords = new double[data[0].length*3];
nioCoords.position(0);
nioCoords.get(coords);
gi.setCoordinates(coords);
// generate normals
NormalGenerator ng = new NormalGenerator();
ng.generateNormals(gi);
Vector3f[] n3f = gi.getNormals();
int[] ni = gi.getNormalIndices();
float[] tmp = new float[3];
for (int i=0; i<ni.length; ++i) {
n3f[ni[i]].get(tmp);
nioNormals.put(tmp);
}
ta.setNormalRefBuffer(new J3DBuffer(nioNormals));
// Setup color buffer
ta.setColorRefBuffer(new J3DBuffer(nioColors));
ta.setCapability(TriangleArray.ALLOW_COLOR_WRITE);
ta.setCapabilityIsFrequent(TriangleArray.ALLOW_COLOR_WRITE);
Shape3D shape3d = new Shape3D(ta);
// The appearance of a shape has many interesting features.
// In particular, in case one wants to display lines (edges) over the shape,
// it is possible to set a polygon offset to the triangles. This offset is in fact
// a distance in the Z-buffer => an offset of 1 for example on the triangles,
// will place them behind the edges. @see PolygonAttributes.setPolygonOffset,
// and the code in syn3d.nodes.java3d.Shape3DJava3D
// Here the shape is created without an appearance and it is managed
// by ShapeNodeJava3D. See this class for more information
shape3d.setCapability(Shape3D.ALLOW_APPEARANCE_READ);
shape3d.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE);
// The shape will be pickable, which means the user can click on the shape in 3D
// space from the 2D window
PickTool.setCapabilities(shape3d,PickTool.INTERSECT_FULL);
shape3d.setCapability(Shape3D.ALLOW_PICKABLE_READ);
shape3d.setCapability(Shape3D.ALLOW_PICKABLE_WRITE);
shape3d.setPickable(true);
// Create a node in the JTree for this shape
new ShapeNodeJava3D(this, shape3d);
// Syn3D picking code uses the user data of a Java3D object to find back a node
// that handles the picking. Let's declare this object as the handler instead of
// ShapeNodeJava3D
shape3d.setUserData(this);
// scale the shape to have a reasonable size
// and translate it to the origin
Bounds bounds = transformGroup.getBounds();
if (bounds instanceof BoundingSphere) {
Point3d center = new Point3d();
((BoundingSphere)bounds).getCenter(center);
Transform3D tr = new Transform3D();
tr.setScale(10.0/((BoundingSphere)bounds).getRadius());
Transform3D tr2 = new Transform3D();
center.negate();
tr2.setTranslation(new Vector3f(center));
tr.mul(tr2);
transformGroup.setTransform(tr);
}
// Cleanup file name, use it for this node
fileName = file.getName();
fileName = fileName.substring(0, fileName.indexOf('.'));
setName(fileName);
// now set up the colors using one of the variables => first one by default
showResult(0);
}
// see parent comments.
// This method is called directly by the plugin on key press, as an example how to do it
public void showResult(int num) {
// Get the shape back from the children list. Ignore case when there is no file loaded
if (children.size()<1) return;
Shape3D shape3d = (Shape3D)((ShapeNodeJava3D)children.get(0)).get3DObject();
// Find the color buffer
FloatBuffer colors = (FloatBuffer)(((TriangleArray)shape3d.getGeometry()).getColorRefBuffer().getBuffer());
// helper variables
int d = num+3;
if (d>=names.length) return; // ignore keypressed events for unknown variables
currentVariable = d;
float[] color = new float[3];
// Update the buffer
for (int i=0; i<triangles.length; i+=3) {
getColorForValue(data[d][triangles[i]]).getRGBColorComponents(color);
colors.position(i*3);
colors.put(color);
getColorForValue(data[d][triangles[i+1]]).getRGBColorComponents(color);
colors.position((i+1)*3);
colors.put(color);
getColorForValue(data[d][triangles[i+2]]).getRGBColorComponents(color);
colors.position((i+2)*3);
colors.put(color);
}
// Update the name in the tree to show the selected variable
((ActiveNode)children.get(0)).setName(names[d]);
// Syn3D internal event system. Handles refreshing of the scene and the JTree, when
// they are visible.
notifyInternalChange();
}
// see comments on super implementation
// To make it short : this is the method receiving picking events, for the 3D shape that
// declared this object as its user data.
public void highlight(boolean on, Object parameter) {
// When picking, the parameter is always a PickResult
// When selecting from the JTree, the parameter is not a PickResult
// => ignore the JTree selection of the whole shape in this example
if (!(parameter instanceof PickResult)) return;
PickResult result = (PickResult)parameter;
result.setFirstIntersectOnly(true);
PickIntersection pi = result.getIntersection(0);
// indices of the picked triangle
// should always be triangle at this point
// Indices are set to vertex indices, as this is not an Index Geometry object
int[] idx = pi.getPrimitiveColorIndices();
FloatBuffer colors = (FloatBuffer)(pi.getGeometryArray()).getColorRefBuffer().getBuffer();
float[] color = new float[3];
// The index returned by this method is relative the the array returned above,
// not the shape geometry array!
int closest = pi.getClosestVertexIndex();
// This method is called both when highlighting the shape and when de-selecting it
// => handles both, so as to restore the original colors
// Tip : try to hold CTRL while picking elements, for multiple selection
// => this method is called exactly once for each element state change
if (on) {
// Choose a color for highlighting that is never used usually
//Color.getHSBColor(5f/6f,1,1).getRGBColorComponents(color);
// Let's do something more useful as an example : also highlight the
// closest point in the picked triangle using the color saturation.
// Tip: when using indexed geometry, one could change only the color of the
// closest point and this would affect all triangles sharing it. On the other
// hand, many applications need the selection of a triangle (or quad), and
// don't want side effects on neighboring triangles.
// This method gives a way to do both point and tiangle selection as an example.
// It should be quite straightforward to adapt according to the needs of a
// real application.
getColorForValue(data[currentVariable][triangles[idx[0]]],(closest==0) ? 0.1f : 0.5f).getRGBColorComponents(color);
colors.position(idx[0]*3);
colors.put(color);
getColorForValue(data[currentVariable][triangles[idx[1]]],(closest==1) ? 0.1f : 0.5f).getRGBColorComponents(color);
colors.position(idx[1]*3);
colors.put(color);
getColorForValue(data[currentVariable][triangles[idx[2]]],(closest==2) ? 0.1f : 0.5f).getRGBColorComponents(color);
colors.position(idx[2]*3);
colors.put(color);
// Also display the selected values to standard output
System.out.print("Values for the picked triangle ("+names[currentVariable]+"): ");
System.out.print(data[currentVariable][triangles[idx[0]]]);
System.out.print(closest==0 ? " * " : " " );
System.out.print(data[currentVariable][triangles[idx[1]]]);
System.out.print(closest==1 ? " * " : " " );
System.out.print(data[currentVariable][triangles[idx[2]]]);
System.out.print(closest==2 ? " * " : " " );
System.out.println(" with * = closest point");
}
else {
// restore original colors
getColorForValue(data[currentVariable][triangles[idx[0]]]).getRGBColorComponents(color);
colors.position(idx[0]*3);
colors.put(color);
getColorForValue(data[currentVariable][triangles[idx[1]]]).getRGBColorComponents(color);
colors.position(idx[1]*3);
colors.put(color);
getColorForValue(data[currentVariable][triangles[idx[2]]]).getRGBColorComponents(color);
colors.position(idx[2]*3);
colors.put(color);
}
// Syn3D event propagation, refresh the scene, etc...
super.highlight(on,parameter);
}
// Helper method: Return the pure color for the given value using the currently selected
// variable.
protected Color getColorForValue(double value) {
return getColorForValue(value, 1f);
}
// Helper method: Return the color for the given value using the currently selected variable
// but also apply a saturation modifier.
protected Color getColorForValue(double value, float saturation) {
double hue = 2.0 * (max[currentVariable] - value) / ( 3.0 * (max[currentVariable] - min[currentVariable]));
return Color.getHSBColor((float)hue, saturation, 1.0f);
}
}