package framework.model3D; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; import java.util.Iterator; import javax.media.j3d.Appearance; import javax.media.j3d.DirectionalLight; import javax.media.j3d.Geometry; import javax.media.j3d.GeometryArray; import javax.media.j3d.IndexedQuadArray; import javax.media.j3d.IndexedTriangleArray; import javax.media.j3d.IndexedTriangleFanArray; import javax.media.j3d.IndexedTriangleStripArray; import javax.media.j3d.Light; import javax.media.j3d.Material; import javax.media.j3d.Node; import javax.media.j3d.PointLight; import javax.media.j3d.PolygonAttributes; import javax.media.j3d.RenderingAttributes; import javax.media.j3d.Shape3D; import javax.media.j3d.Transform3D; import javax.media.j3d.TransparencyAttributes; import javax.media.j3d.TriangleFanArray; import javax.media.j3d.TriangleStripArray; import javax.vecmath.Point3d; import javax.vecmath.Point3f; import javax.vecmath.Vector3d; import javax.vecmath.Vector3f; import com.sun.j3d.utils.geometry.Box; import com.sun.j3d.utils.geometry.Cone; import com.sun.j3d.utils.geometry.Cylinder; import com.sun.j3d.utils.geometry.Sphere; /** * ステンシルバッファを用いたシャドウボリューム * @author 新田直也 * */ public class ShadowVolume { private static final boolean WIRE_FRAME_VIEW = false; private Light light = null; private double shadowDepth = 10.0; private IndexedQuadArray volumeGeometry = null; private Shape3D shadowVolumeFront = null; private Shape3D shadowVolumeBack = null; private int vertexCount = 0; private double coordinates[] = null; private int indicies[] = null; private Hashtable<Integer, Edge> edges = null; private ArrayList<Triangle> triangles = null; /** * 平行光源用のシャドウボリューム * @param occuluder 遮蔽オブジェクト(影を落とすオブジェクト) * @param light 光源 * @param shadowDepth 影の深さ * @param localToWorld 遮蔽オブジェクト用の座標変換 */ public ShadowVolume(BaseObject3D occuluder, Light light, double shadowDepth, Transform3D localToWorld) { this.light = light; this.shadowDepth = shadowDepth; // 幾何情報の抽出 if (!extractGeometricInformation(occuluder)) return; // シャドウボリュームの表面を生成 volumeGeometry = createVolumeSurface(light, shadowDepth, localToWorld, coordinates, edges, triangles); // シャドウボリュームオブジェクトの生成 createShadowVolumeObject(volumeGeometry, occuluder); } /** * シャドウボリュームの更新 * @param localToWorld 遮蔽オブジェクト用の座標変換 */ public void update(Transform3D localToWorld) { // シャドウボリュームの表面を再生成 volumeGeometry = createVolumeSurface(light, shadowDepth, localToWorld, coordinates, edges, triangles); // シャドウボリュームオブジェクトの更新 shadowVolumeFront.removeAllGeometries(); shadowVolumeFront.addGeometry(volumeGeometry); shadowVolumeBack.removeAllGeometries(); shadowVolumeBack.addGeometry(volumeGeometry); } /** * 幾何情報の抽出 * @param shadowCastingObject 抽出元のオブジェクト * @return true --- 正常に抽出できた, false --- 正常に抽出できなかった */ private boolean extractGeometricInformation(BaseObject3D shadowCastingObject) { Node node = shadowCastingObject.getPrimitiveNode(); if (node == null) return false; // GeometryArrayの習得 ArrayList<Geometry> geometries = new ArrayList<Geometry>(); Geometry g = null; if (node instanceof Shape3D) { g = ((Shape3D)node).getGeometry(); geometries.add(g); } else if (node instanceof Cone) { g = ((Cone)node).getShape(Cone.BODY).getGeometry(); geometries.add(g); g = ((Cone)node).getShape(Cone.CAP).getGeometry(); geometries.add(g); } else if (node instanceof Cylinder) { g = ((Cylinder)node).getShape(Cylinder.BODY).getGeometry(); geometries.add(g); g = ((Cylinder)node).getShape(Cylinder.TOP).getGeometry(); geometries.add(g); g = ((Cylinder)node).getShape(Cylinder.BOTTOM).getGeometry(); geometries.add(g); } else if (node instanceof Box) { g = ((Box)node).getShape(Box.TOP).getGeometry(); geometries.add(g); g = ((Box)node).getShape(Box.BOTTOM).getGeometry(); geometries.add(g); g = ((Box)node).getShape(Box.FRONT).getGeometry(); geometries.add(g); g = ((Box)node).getShape(Box.BACK).getGeometry(); geometries.add(g); g = ((Box)node).getShape(Box.LEFT).getGeometry(); geometries.add(g); g = ((Box)node).getShape(Box.RIGHT).getGeometry(); geometries.add(g); } else if (node instanceof Sphere) { g = ((Sphere)node).getShape().getGeometry(); geometries.add(g); } else { return false; } // 頂点の座標値を取得し、設定 vertexCount = 0; for (int n = 0; n < geometries.size(); n++) { g = geometries.get(n); if (g instanceof GeometryArray) { // GeometryArray の場合 GeometryArray geometryArray = (GeometryArray)g; vertexCount += geometryArray.getVertexCount(); } } coordinates = new double[vertexCount * 3 * 2]; indicies = new int[vertexCount * 4 * 3]; // 辺の数を頂点の数の最大3倍と考えている int base = 0; for (int n = 0; n < geometries.size(); n++) { g = geometries.get(n); if (g instanceof GeometryArray) { // GeometryArray の場合 GeometryArray geometryArray = (GeometryArray)g; double coordinate[] = new double[3]; int count = geometryArray.getVertexCount(); for (int v = 0; v < count; v++) { geometryArray.getCoordinate(v, coordinate); coordinates[(v + base) * 3] = coordinate[0]; coordinates[(v + base) * 3 + 1] = coordinate[1]; coordinates[(v + base) * 3 + 2] = coordinate[2]; } base += count; } } // 面と辺の情報を作成 edges = new Hashtable<Integer, Edge>(); triangles = new ArrayList<Triangle>(); base = 0; for (int n = 0; n < geometries.size(); n++) { g = geometries.get(n); if (g instanceof GeometryArray) { // GeometryArray の場合 GeometryArray geometryArray = (GeometryArray)g; Point3d p1 = new Point3d(); Point3d p2 = new Point3d(); Point3d p3 = new Point3d(); int v1, v2, v3; if (g instanceof TriangleStripArray) { // TriangleStripArray の場合 TriangleStripArray triStripArray = (TriangleStripArray)g; int num = triStripArray.getNumStrips(); int strips[] = new int[num]; triStripArray.getStripVertexCounts(strips); int index = base; for (int j = 0; j < num; j++) { index += 2; for (int i = 2; i < strips[j]; i += 2) { addTriangle(index - 2, index - 1, index, p1, p2, p3); if (i <= strips[j] - 2) { addTriangle(index - 1, index + 1, index, p1, p2, p3); index += 1; } index += 1; } } } else if (g instanceof TriangleFanArray) { // TriangleFanArray の場合 TriangleFanArray triFanArray = (TriangleFanArray)g; int num = triFanArray.getNumStrips(); int strips[] = new int[num]; triFanArray.getStripVertexCounts(strips); int index = base; for (int j = 0; j < num; j++) { v1 = index; index += 1; for (int i = 1; i < strips[j] - 1; i += 1) { addTriangle(v1, index, index + 1, p1, p2, p3); index += 1; } } } else if (g instanceof IndexedTriangleArray) { // IndexedTriangleArray の場合 IndexedTriangleArray triArray = (IndexedTriangleArray)g; int indexCount = triArray.getIndexCount(); for (int i = 0; i < indexCount; i += 3) { v1 = triArray.getCoordinateIndex(i) + base; v2 = triArray.getCoordinateIndex(i + 1) + base; v3 = triArray.getCoordinateIndex(i + 2) + base; addTriangle(v1, v2, v3, p1, p2, p3); } } else if (g instanceof IndexedTriangleStripArray) { // IndexedTriangleStripArray の場合 IndexedTriangleStripArray triStripArray = (IndexedTriangleStripArray)g; int num = triStripArray.getNumStrips(); int strips[] = new int[num]; triStripArray.getStripIndexCounts(strips); int index = 0; for (int j = 0; j < num; j++) { index += 2; for (int i = 2; i < strips[j]; i += 2) { v1 = triStripArray.getCoordinateIndex(index - 2) + base; v2 = triStripArray.getCoordinateIndex(index - 1) + base; v3 = triStripArray.getCoordinateIndex(index) + base; addTriangle(v1,v2, v3, p1, p2, p3); if (i <= strips[j] - 2) { v1 = v2; v2 = triStripArray.getCoordinateIndex(index + 1) + base; addTriangle(v1, v2, v3, p1, p2, p3); index += 1; } index += 1; } } } else if (g instanceof IndexedTriangleFanArray) { // IndexedTriangleFanArray の場合 IndexedTriangleFanArray triFanArray = (IndexedTriangleFanArray)g; int num = triFanArray.getNumStrips(); int strips[] = new int[num]; triFanArray.getStripIndexCounts(strips); int index = 0; for (int j = 0; j < num; j++) { v1 = triFanArray.getCoordinateIndex(index) + base; index += 1; for (int i = 1; i < strips[j] - 1; i += 1) { v2 = triFanArray.getCoordinateIndex(index) + base; v3 = triFanArray.getCoordinateIndex(index + 1) + base; addTriangle(v1, v2, v3, p1, p2, p3); index += 1; } } } base += geometryArray.getVertexCount(); } } return true; } private void addTriangle(int v1, int v2, int v3, Point3d p1, Point3d p2, Point3d p3) { Vector3d vec1; Vector3d vec2; Vector3d vec; Triangle tri; Edge e; p1.x = coordinates[v1 * 3]; p1.y = coordinates[v1 * 3 + 1]; p1.z = coordinates[v1 * 3 + 2]; p2.x = coordinates[v2 * 3]; p2.y = coordinates[v2 * 3 + 1]; p2.z = coordinates[v2 * 3 + 2]; p3.x = coordinates[v3 * 3]; p3.y = coordinates[v3 * 3 + 1]; p3.z = coordinates[v3 * 3 + 2]; vec = new Vector3d(p1); vec1 = new Vector3d(p2); vec2 = new Vector3d(p3); vec1.sub(vec); vec2.sub(vec); vec1.cross(vec1, vec2); if (vec1.length() < GeometryUtility.TOLERANCE) { return; } vec1.normalize(); tri = new Triangle(v1, v2, v3, vec1); triangles.add(tri); e = edges.get(new Integer(v2 * vertexCount + v1)); // 逆向きに回っている辺のみ共有 if (e != null) { tri.edges[0] = e; e.triangles[1] = tri; } else { vec = new Vector3d(p2); vec.sub(p1); e = new Edge(v1, v2, vec); tri.edges[0] = e; e.triangles[0] = tri; edges.put(new Integer(v1 * vertexCount + v2), e); } e = edges.get(new Integer(v3 * vertexCount + v2)); // 逆向きに回っている辺のみ共有 if (e != null) { tri.edges[1] = e; e.triangles[1] = tri; } else { vec = new Vector3d(p3); vec.sub(p2); e = new Edge(v2, v3, vec); tri.edges[1] = e; e.triangles[0] = tri; edges.put(new Integer(v2 * vertexCount + v3), e); } e = edges.get(new Integer(v1 * vertexCount + v3)); // 逆向きに回っている辺のみ共有 if (e != null) { tri.edges[2] = e; e.triangles[1] = tri; } else { vec = new Vector3d(p1); vec.sub(p3); e = new Edge(v3, v1, vec); tri.edges[2] = e; e.triangles[0] = tri; edges.put(new Integer(v3 * vertexCount + v1), e); } } /** * シャドウボリュームのジオメトリの作成 * @param light * @param shadowDepth * @param localToWorld * @param coordinates * @param edges * @param triangles * @return シャドウボリュームのジオメトリ */ private IndexedQuadArray createVolumeSurface(Light light, double shadowDepth, Transform3D localToWorld, double[] coordinates, Hashtable<Integer, Edge> edges, ArrayList<Triangle> triangles) { // オブジェクトに対する相対座標系に変換 Vector3d displacement = null; Point3d localLightPosition = null; Transform3D worldToLocal; Point3d worldLightPosition = null; if (light instanceof DirectionalLight) { // 平行光源の場合 Vector3f lightDirectionF = new Vector3f(); ((DirectionalLight)light).getDirection(lightDirectionF); displacement = new Vector3d(lightDirectionF); displacement.scale(shadowDepth); worldToLocal = new Transform3D(localToWorld); worldToLocal.invert(); worldToLocal.transform(displacement); } else if (light instanceof PointLight) { // 点光源の場合 Point3f worldLightPositionF = new Point3f(); ((PointLight)light).getPosition(worldLightPositionF); worldLightPosition = new Point3d(worldLightPositionF); localLightPosition = new Point3d(worldLightPosition); worldToLocal = new Transform3D(localToWorld); worldToLocal.invert(); worldToLocal.transform(localLightPosition); } else { return null; } Triangle tri; // すべての面の光線に対する向きを計算 if (displacement != null) { for (int n = 0; n < triangles.size(); n++) { tri = triangles.get(n); tri.innerProduct = tri.normal.dot(displacement); } } else if (localLightPosition != null) { for (int n = 0; n < triangles.size(); n++) { tri = triangles.get(n); Vector3d v = new Vector3d(coordinates[tri.v1*3], coordinates[tri.v1*3 + 1], coordinates[tri.v1*3 + 2]); v.sub(localLightPosition); tri.innerProduct = tri.normal.dot(v); } } Collection<Edge> es = edges.values(); Edge edge; int index = 0; boolean doesGenerateSurface = false; double i0 = 0, i1 = 0; Vector3d displacement1 = displacement; Vector3d displacement2 = displacement; Vector3d tempVec = new Vector3d(); Point3d worldPos = new Point3d(); if (localLightPosition != null) { // 点光源の場合 displacement1 = new Vector3d(); displacement2 = new Vector3d(); } for (Iterator<Edge> it = es.iterator(); it.hasNext(); ) { edge = it.next(); // edgeに対してシャドウボリュームの表面を生成するか否かの判定 doesGenerateSurface = false; if (edge.triangles[1] == null) { // 開多面体の場合 i0 = edge.triangles[0].innerProduct; if (i0 < -GeometryUtility.TOLERANCE) { // 光源に対して表向きの面の辺に対してのみ生成 doesGenerateSurface = true; // 生成する } } else { i0 = edge.triangles[0].innerProduct; i1 = edge.triangles[1].innerProduct; if (i0 * i1 < GeometryUtility.TOLERANCE) { if (i0 * i1 > -GeometryUtility.TOLERANCE) { // 辺を挟む2つの面の一方が光線の向きに対して平行である if (i0 < -GeometryUtility.TOLERANCE || i1 < -GeometryUtility.TOLERANCE) { // いずれかの面が表向きだった場合 tempVec.cross(edge.triangles[0].normal, edge.triangles[1].normal); if (edge.v1ToV2.dot(tempVec) > GeometryUtility.TOLERANCE) { // 辺が凸の場合のみ生成 doesGenerateSurface = true; // 生成する } } } else { // 辺を挟む2つの面が光線の向きに対して裏表である doesGenerateSurface = true; // 生成する } } } if (doesGenerateSurface) { // シャドウボリュームの表面を生成する if (localLightPosition != null) { // 点光源の場合 displacement1.x = coordinates[edge.v1 * 3] - localLightPosition.x; displacement1.y = coordinates[edge.v1 * 3 + 1] - localLightPosition.y; displacement1.z = coordinates[edge.v1 * 3 + 2] - localLightPosition.z; worldPos.x = coordinates[edge.v1 * 3]; worldPos.y = coordinates[edge.v1 * 3 + 1]; worldPos.z = coordinates[edge.v1 * 3 + 2]; localToWorld.transform(worldPos); displacement1.scale(shadowDepth / worldPos.distance(worldLightPosition)); displacement2.x = coordinates[edge.v2 * 3] - localLightPosition.x; displacement2.y = coordinates[edge.v2 * 3 + 1] - localLightPosition.y; displacement2.z = coordinates[edge.v2 * 3 + 2] - localLightPosition.z; worldPos.x = coordinates[edge.v2 * 3]; worldPos.y = coordinates[edge.v2 * 3 + 1]; worldPos.z = coordinates[edge.v2 * 3 + 2]; localToWorld.transform(worldPos); displacement2.scale(shadowDepth / worldPos.distance(worldLightPosition)); } if (i0 < -GeometryUtility.TOLERANCE) { // 表を向いている面はedge.triangles[0]、edgeの向きは常にedge.triangles[0]の辺の向きと一致するので、 // edgeの向きに従ってシャドウボリュームの表面を生成 indicies[index] = edge.v2; indicies[index + 1] = edge.v1; indicies[index + 2] = edge.v1 + vertexCount; indicies[index + 3] = edge.v2 + vertexCount; } else { // 表を向いている面はedge.triangles[1]、edgeの向きは常にedge.triangles[1]の辺の向きと反対なので、 // edgeの向きと逆向きにシャドウボリュームの表面を生成 indicies[index] = edge.v1; indicies[index + 1] = edge.v2; indicies[index + 2] = edge.v2 + vertexCount; indicies[index + 3] = edge.v1 + vertexCount; } coordinates[(edge.v1 + vertexCount) * 3] = coordinates[edge.v1 * 3] + displacement1.x; coordinates[(edge.v1 + vertexCount) * 3 + 1] = coordinates[edge.v1 * 3 + 1] + displacement1.y; coordinates[(edge.v1 + vertexCount) * 3 + 2] = coordinates[edge.v1 * 3 + 2] + displacement1.z; coordinates[(edge.v2 + vertexCount) * 3] = coordinates[edge.v2 * 3] + displacement2.x; coordinates[(edge.v2 + vertexCount) * 3 + 1] = coordinates[edge.v2 * 3 + 1] + displacement2.y; coordinates[(edge.v2 + vertexCount) * 3 + 2] = coordinates[edge.v2 * 3 + 2] + displacement2.z; index += 4; } } if (index == 0) return null; int validIndicies[] = new int[index]; System.arraycopy(indicies, 0, validIndicies, 0, index); volumeGeometry = new IndexedQuadArray(coordinates.length / 3, IndexedQuadArray.COORDINATES | IndexedQuadArray.BY_REFERENCE, index); volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_READ); volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_WRITE); volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_INDEX_READ); volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_INDEX_WRITE); volumeGeometry.setCoordRefDouble(coordinates); volumeGeometry.setCoordinateIndices(0, validIndicies); return volumeGeometry; } /** * シャドウボリュームの生成 * @param volumeGeometry * @param occuluder */ private void createShadowVolumeObject(IndexedQuadArray volumeGeometry, BaseObject3D occuluder) { // シャドウボリュームの表面のオブジェクトの生成 Appearance frontAp = new Appearance(); if (WIRE_FRAME_VIEW) frontAp.setMaterial(new Material()); RenderingAttributes frontRA = new RenderingAttributes(); if (!WIRE_FRAME_VIEW) frontRA.setStencilEnable(true); // ステンシルバッファを用いる(ただし、Canvas3Dで適切な設定がされている必要がある) if (!WIRE_FRAME_VIEW) frontRA.setDepthBufferWriteEnable(false); // Zバッファを更新しない frontRA.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // Zバッファの影響を受ける frontRA.setStencilFunction(RenderingAttributes.ALWAYS, 0, ~0); // ステンシルバッファの影響を受けない frontRA.setStencilOp(RenderingAttributes.STENCIL_KEEP, RenderingAttributes.STENCIL_KEEP, RenderingAttributes.STENCIL_INCR); // 見えているピクセルのみステンシルバッファの値を増やす frontAp.setRenderingAttributes(frontRA); PolygonAttributes pa1 = new PolygonAttributes(); if (WIRE_FRAME_VIEW) pa1.setPolygonMode(PolygonAttributes.POLYGON_LINE); pa1.setCullFace(PolygonAttributes.CULL_BACK); // 裏面を処理しない frontAp.setPolygonAttributes(pa1); if (!WIRE_FRAME_VIEW) { TransparencyAttributes frontTA = new TransparencyAttributes(); // setVisible(false)だとステンシルバッファが更新されないので、透明にする frontTA.setTransparencyMode(TransparencyAttributes.BLENDED); frontTA.setTransparency(1.0f); frontAp.setTransparencyAttributes(frontTA); } shadowVolumeFront = new Shape3D(volumeGeometry, frontAp); shadowVolumeFront.setCapability(Shape3D.ALLOW_GEOMETRY_READ); shadowVolumeFront.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); occuluder.center.addChild(shadowVolumeFront); // シャドウボリュームの裏面のオブジェクトの生成 Appearance backAp= new Appearance(); if (WIRE_FRAME_VIEW) backAp.setMaterial(new Material()); RenderingAttributes backRA= new RenderingAttributes(); if (!WIRE_FRAME_VIEW) backRA.setStencilEnable(true); // ステンシルバッファを用いる(ただし、Canvas3Dで適切な設定がされている必要がある) if (!WIRE_FRAME_VIEW) backRA.setDepthBufferWriteEnable(false); // Zバッファを更新しない backRA.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // Zバッファの影響を受ける backRA.setStencilFunction(RenderingAttributes.ALWAYS, 0, ~0); // ステンシルバッファの影響を受けない backRA.setStencilOp(RenderingAttributes.STENCIL_KEEP, RenderingAttributes.STENCIL_KEEP, RenderingAttributes.STENCIL_DECR); // 見えているピクセルのみステンシルバッファの値を減らす backAp.setRenderingAttributes(backRA); PolygonAttributes pa2 = new PolygonAttributes(); if (WIRE_FRAME_VIEW) pa2.setPolygonMode(PolygonAttributes.POLYGON_LINE); pa2.setCullFace(PolygonAttributes.CULL_FRONT); // 表面を処理しない backAp.setPolygonAttributes(pa2); if (!WIRE_FRAME_VIEW) { TransparencyAttributes backTA = new TransparencyAttributes(); // setVisible(false)だとステンシルバッファが更新されないので、透明にする backTA.setTransparencyMode(TransparencyAttributes.BLENDED); backTA.setTransparency(1.0f); backAp.setTransparencyAttributes(backTA); } shadowVolumeBack = new Shape3D(volumeGeometry, backAp); shadowVolumeBack.setCapability(Shape3D.ALLOW_GEOMETRY_READ); shadowVolumeBack.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); occuluder.center.addChild(shadowVolumeBack); } private class Triangle { Edge edges[] = new Edge[3]; Vector3d normal; double innerProduct; int v1, v2, v3; Triangle(int v1, int v2, int v3, Vector3d normal) { this.v1 = v1; this.v2 = v2; this.v3 = v3; this.normal = normal; } } private class Edge { Triangle triangles[] = new Triangle[2]; int v1, v2; Vector3d v1ToV2; // Vector3d normal = null; Edge(int v1, int v2, Vector3d v1ToV2) { this.v1 = v1; this.v2 = v2; this.v1ToV2 = v1ToV2; } // Vector3d normal() { // if (normal != null) return normal; // normal = new Vector3d(triangles[0].normal); // if (triangles[1] != null) { // normal.add(triangles[1].normal); // normal.scale(0.5); // } // return normal; // } } }