/** * Copyright (c) 2003-2009, Xith3D Project Group all rights reserved. * * Portions based on the Java3D interface, Copyright by Sun Microsystems. * Many thanks to the developers of Java3D and Sun Microsystems for their * innovation and design. * * 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 the 'Xith3D Project Group' 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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) A * RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE */ package org.xith3d.effects.shadows.occluder; import java.util.Arrays; import java.util.List; import java.util.TreeMap; import org.openmali.vecmath2.Point3f; import org.xith3d.scenegraph.Geometry; import org.xith3d.scenegraph.Transform3D; import org.xith3d.scenegraph.TriangleArray; import org.xith3d.scenegraph.utils.GeomDrawUtil; import org.xith3d.utility.comparator.PointComparator; import org.xith3d.utility.logging.X3DLog; /** * An occluder is a special construction of the geometry of a piece in the scene * graph. It consists of a set of faces with connectivity information and a * spatial container which partitions the faces such that they can be quickly * used in collision checks. An occluder is a primary object for shadow volumes * as we can easily calculate the proper edges needed for silhouettes.<br> * <br> * An occluder is attached to a node in the scenegraph and from that point * forward represents the geometry lying below that portion of the scene. It * does duplicate some aspects of the GeometryArrays in the shapes below the * occluder, but the occluder can be polygon reduced to have a less dense set of * triangles.<br> * <br> * it is important to note that occluders should be fully contiguous shapes * such that there are no orphan edges.<br> * <br> * One thing that can cause issues is the fact that the scenegraph which is * represented by a single occluder could actually contain nested transforms * which within the local coordinate space of the model. These nested transforms * are flattened when building the occluder so that the proper vertices can * align.<br> * <br> * The next problem is that there could be a transform above the model which * moves in real time. This would of course normally effect the faces within * their local space, as well as their plane calculations. In order to bypass * this we will be transforming the location of the lightsource into the local * coordinate space so that we can determine the edges. Then when we render the * shadow we will use the normal local to world transform for the shadow. (I * thought of this in a dream btw... ran downstairs to write it down - Yazel).<br> * * @author David Yazel */ public class Occluder { private class Coord implements Comparable< Coord > { int index; Point3f point; public Coord( Point3f point ) { this.point = point; } /** * {@inheritDoc} */ @Override public int hashCode() { // for our special case the hashCode must not "contain" the index. return ( point.hashCode() ); } public boolean equals( Coord o ) { Point3f p = o.point; return ( PointComparator.comparePoints( point, p, 0.001f ) == 0 ); } @Override public boolean equals( Object o ) { if ( !( o instanceof Coord ) ) return ( false ); return ( equals( (Coord)o ) ); } /** * {@inheritDoc} */ public int compareTo( Coord o ) { return ( PointComparator.comparePoints( point, o.point, 0.001f ) ); } } public Transform3D worldTransform; public int nVertices; public int nFaces; // for caching private Point3f lastLightPosition = new Point3f(); private Transform3D lastTransform = new Transform3D(); /* while it makes logical sense to store the occluder information in * objects, this makes for very slow loading from disk. We will store the * information in multiple arrays, one per element of the OccluderFace. */ // Index Of Each Vertex Within An Object That Makes Up The Triangle Of This // Face public int[] vertexIndices; // 4 elements per face Index Of Each Face That Neighbours This One Within // The Object, 3 elements per face public float[] planeEquation; public int[] neighbourIndices; // one element per face public boolean[] visible; // actual coordinate data, unique points only public float[] vertices; public Geometry buffer = null; /** * Creates a new Occluder. */ public Occluder() { } public Geometry getBuffer() { return ( buffer ); } public void determineVisibleEdges( Point3f lightPosition ) { if ( ( worldTransform.compareTo( lastTransform ) != 0 ) || ( !lastLightPosition.equals( lightPosition ) ) ) { Point3f lp = new Point3f( lightPosition ); Transform3D t = new Transform3D(); t.set( worldTransform ); t.invert(); t.transform( lp ); // Determine Which Faces Are Visible By The Light. int numVisible = 0; for ( int i = 0; i < nFaces; i++ ) { float side = planeEquation[ i * 4 + 0 ] * lp.getX() + planeEquation[ i * 4 + 1 ] * lp.getY() + planeEquation[ i * 4 + 2 ] * lp.getZ() + planeEquation[ i * 4 + 3 ]; if ( side > 0 ) { visible[ i ] = true; } else { visible[ i ] = false; numVisible++; } } // build the buffer for rendering if ( buffer == null ) buffer = new TriangleArray( nFaces * 3 * 6 ); GeomDrawUtil drawer = new GeomDrawUtil( buffer ); final float INFINITY = 10000; int numFragments = 0; drawer.drawStart( GeomDrawUtil.CHANGE_COORDINATES ); for ( int i = 0; i < nFaces; i++ ) { if ( visible[ i ] ) { // Go Through Each Edge for ( int j = 0; j < 3; j++ ) { int neighbourIndex = neighbourIndices[ i * 3 + j ]; // If There Is No Neighbour, Or Its Neighbouring Face Is // Not Visible, Then This Edge Casts A Shadow if ( neighbourIndex == -1 || ( visible[ neighbourIndex ] == false ) ) { numFragments++; // Get The Points On The Edge float v1x = vertices[ vertexIndices[ i * 3 + j ] * 3 + 0 ]; float v1y = vertices[ vertexIndices[ i * 3 + j ] * 3 + 1 ]; float v1z = vertices[ vertexIndices[ i * 3 + j ] * 3 + 2 ]; int k = ( j + 1 ) % 3; float v2x = vertices[ vertexIndices[ i * 3 + k ] * 3 + 0 ]; float v2y = vertices[ vertexIndices[ i * 3 + k ] * 3 + 1 ]; float v2z = vertices[ vertexIndices[ i * 3 + k ] * 3 + 2 ]; // Calculate The Two Vertices In Distance float v3x = ( v1x - lp.getX() ) * INFINITY; float v3y = ( v1y - lp.getY() ) * INFINITY; float v3z = ( v1z - lp.getZ() ) * INFINITY; float v4x = ( v2x - lp.getX() ) * INFINITY; float v4y = ( v2y - lp.getY() ) * INFINITY; float v4z = ( v2z - lp.getZ() ) * INFINITY; // Draw The Quadrilateral (As A Triangle Strip) drawer.setCoordinate( v1x, v1y, v1z ); drawer.setCoordinate( v1x + v3x, v1y + v3y, v1z + v3z ); drawer.setCoordinate( v2x + v4x, v2y + v4y, v2z + v4z ); drawer.setCoordinate( v1x, v1y, v1z ); drawer.setCoordinate( v2x + v4x, v2y + v4y, v2z + v4z ); drawer.setCoordinate( v2x, v2y, v2z ); } } } } // TODO : Check // buffer.setValidVertexCount(numFragments*6); buffer.setValidVertexCount( buffer.getCoordinatesData().getCount() / 3 ); } } /** * Takes a list of occluder submission nodes and builds the vertex and face * lists * * @param submission A list of OccluderSubmission nodes */ public void build( List< OccluderSubmission > submission ) { // step through and determine the total number of faces and // vertices we need to allocate; nVertices = 0; nFaces = 0; for ( OccluderSubmission os: submission ) { nVertices += os.shape.getGeometry().getVertexCount(); } nFaces = nVertices / 3; // allocate face based arays vertexIndices = new int[ nFaces * 3 ]; planeEquation = new float[ nFaces * 4 ]; neighbourIndices = new int[ nFaces * 3 ]; visible = new boolean[ nFaces ]; Arrays.fill( neighbourIndices, -1 ); /* * now we have the number of faces, but we don't know how many unique * vertices we have, so we need to build a unique list of them. This is * a bit expensive, but it would be more expensive to keep all those * vertices around; */ Point3f p = new Point3f(); Coord testCoord = new Coord( p ); TreeMap< Coord, Coord > vertexMap = new TreeMap< Coord, Coord >(); for ( OccluderSubmission os: submission ) { Geometry ga = os.shape.getGeometry(); // get each point final int j0 = ga.getInitialVertexIndex(); final int shapeVertices = ga.getValidVertexCount(); for ( int j = j0; j < shapeVertices; j++ ) { ga.setCoordinate( j, p ); Coord newCoord = new Coord( new Point3f( p ) ); vertexMap.put( testCoord, newCoord ); assert vertexMap.get( testCoord ) != null : "Cannot find the point just entered :" + p; } } // ok, now we have a map of all unique vertices. We need to now // save them in the data array. We will assign the index values // to the tree nodes so we can look them up in the next pass int vIndex = 0; vertices = new float[ vertexMap.size() * 3 ]; for ( Coord c: vertexMap.values() ) { c.index = vIndex; vertices[ vIndex * 3 + 0 ] = c.point.getX(); vertices[ vIndex * 3 + 1 ] = c.point.getY(); vertices[ vIndex * 3 + 2 ] = c.point.getZ(); vIndex++; } // step back through the faces and get the vertices int curFace = 0; for ( OccluderSubmission os: submission ) { Geometry ga = os.shape.getGeometry(); // step through all the faces in the shape final int shapeVertices = ga.getValidVertexCount(); for ( int j = 0; j < shapeVertices; j++ ) { ga.getCoordinate( j, p ); Coord match = vertexMap.get( testCoord ); if ( match == null ) { final String msg = "Cannot find matching coord map for face " + curFace + " corner " + ( j % 3 ) + " which has coord " + p; //System.err.println( msg ); throw new Error( msg ); } vertexIndices[ j ] = match.index; if ( j % 3 == 2 ) { calculateFacePlane( curFace ); curFace++; } } } calculateConnectivity(); } private void calculateFacePlane( int face ) { // Get Shortened Names For The Vertices Of The Face final Point3f v1 = new Point3f(); final Point3f v2 = new Point3f(); final Point3f v3 = new Point3f(); v1.set( vertices[ vertexIndices[ face * 3 + 0 ] * 3 + 0 ], vertices[ vertexIndices[ face * 3 + 0 ] * 3 + 1 ], vertices[ vertexIndices[ face * 3 + 0 ] * 3 + 2 ] ); v2.set( vertices[ vertexIndices[ face * 3 + 1 ] * 3 + 0 ], vertices[ vertexIndices[ face * 3 + 1 ] * 3 + 1 ], vertices[ vertexIndices[ face * 3 + 1 ] * 3 + 2 ] ); v3.set( vertices[ vertexIndices[ face * 3 + 2 ] * 3 + 0 ], vertices[ vertexIndices[ face * 3 + 2 ] * 3 + 1 ], vertices[ vertexIndices[ face * 3 + 2 ] * 3 + 2 ] ); float a = v1.getY() * ( v2.getZ() - v3.getZ() ) + v2.getY() * ( v3.getZ() - v1.getZ() ) + v3.getY() * ( v1.getZ() - v2.getZ() ); float b = v1.getZ() * ( v2.getX() - v3.getX() ) + v2.getZ() * ( v3.getX() - v1.getX() ) + v3.getZ() * ( v1.getX() - v2.getX() ); float c = v1.getX() * ( v2.getY() - v3.getY() ) + v2.getX() * ( v3.getY() - v1.getY() ) + v3.getX() * ( v1.getY() - v2.getY() ); float d = -( v1.getX() * ( v2.getY() * v3.getZ() - v3.getY() * v2.getZ() ) + v2.getX() * ( v3.getY() * v1.getZ() - v1.getY() * v3.getZ() ) + v3.getX() * ( v1.getY() * v2.getZ() - v2.getY() * v1.getZ() ) ); // save off the plane equation planeEquation[ face * 4 + 0 ] = a; planeEquation[ face * 4 + 1 ] = b; planeEquation[ face * 4 + 2 ] = c; planeEquation[ face * 4 + 3 ] = d; } private void calculateConnectivity() { int numConnected = 0; for ( int faceA = 0; faceA < nFaces; faceA++ ) { for ( int edgeA = 0; edgeA < 3; edgeA++ ) { if ( neighbourIndices[ faceA * 3 + edgeA ] == -1 ) { boolean edgeFound = false; for ( int faceB = 0; faceB < nFaces; faceB++ ) { if ( faceA != faceB ) { for ( int edgeB = 0; edgeB < 3; edgeB++ ) { int vertA1 = vertexIndices[ faceA * 3 + edgeA ]; int vertA2 = vertexIndices[ faceA * 3 + ( edgeA + 1 ) % 3 ]; int vertB1 = vertexIndices[ faceB * 3 + edgeB ]; int vertB2 = vertexIndices[ faceB * 3 + ( edgeB + 1 ) % 3 ]; // Check If They Are Neighbours - IE, The Edges // Are The Same if ( ( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ) ) { neighbourIndices[ faceA * 3 + edgeA ] = faceB; neighbourIndices[ faceB * 3 + edgeB ] = faceA; edgeFound = true; numConnected++; break; } } if ( edgeFound ) break; } } } } } X3DLog.debug( "connected ", numConnected, " edges out of ", nFaces * 3 ); } public void setWorldTransform( Transform3D worldTransform ) { this.worldTransform = worldTransform; } public Transform3D getWorldTransform() { return ( worldTransform ); } public int[] getNeighbourIndices() { return ( neighbourIndices ); } public int getNFaces() { return ( nFaces ); } public int getNVertices() { return ( nVertices ); } public float[] getPlaneEquation() { return ( planeEquation ); } public int[] getVertexIndices() { return ( vertexIndices ); } public float[] getVertices() { return ( vertices ); } public boolean[] getVisible() { return ( visible ); } }