package com.vitco.low.hull; import com.threed.jpct.SimpleVector; import com.vitco.low.CubeIndexer; import gnu.trove.iterator.TIntIterator; import gnu.trove.list.array.TIntArrayList; import gnu.trove.map.hash.TIntShortHashMap; import gnu.trove.set.hash.TIntHashSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; /** * Extended functionality for the hull manager */ public class HullManagerExt<T> extends HullManager<T> implements HullManagerExtInterface { // ===================== // -- ray hit test // ===================== // do a hit test against the voxels in this hull manager @Override public short[] hitTest(SimpleVector origin, SimpleVector dir) { // If the origin is outside the max box of the CubeIndexer it needs to be // shifted into the cube before we can proceed // Note: Not necessary atm (since the camera is usually inside the max box) // origin = CubeIndexer.validateRay(origin, dir); // if (origin == null) { // return null; // } // --------------- // step direction short stepX = (short) Math.signum(dir.x); short stepY = (short) Math.signum(dir.y); short stepZ = (short) Math.signum(dir.z); boolean stepXB = stepX > 0; boolean stepYB = stepY > 0; boolean stepZB = stepZ > 0; short sideX = (short) (stepXB ? 1 : 0); short sideY = (short) (stepYB ? 3 : 2); short sideZ = (short) (stepZB ? 5 : 4); // starting grid coordinates short lastHitSide; int pos = CubeIndexer.getId( (short) Math.floor(origin.x), (short) Math.floor(origin.y), (short) Math.floor(origin.z) ); // compute the offsets double offX = stepX == Math.signum(origin.x) ? (1 - Math.abs(origin.x%1d)) : Math.abs(origin.x%1d); double offY = stepY == Math.signum(origin.y) ? (1 - Math.abs(origin.y%1d)) : Math.abs(origin.y%1d); double offZ = stepZ == Math.signum(origin.z) ? (1 - Math.abs(origin.z%1d)) : Math.abs(origin.z%1d); offX = (double)Math.round(offX * 1000000000) / 1000000000; offY = (double)Math.round(offY * 1000000000) / 1000000000; offZ = (double)Math.round(offZ * 1000000000) / 1000000000; if (offX == 0) { offX = 1; } if (offY == 0) { offY = 1; } if (offZ == 0) { offZ = 1; } // the "progress" value double valYX = Math.abs(dir.y / dir.x); double valZX = Math.abs(dir.z / dir.x); double valZY = Math.abs(dir.z / dir.y); int tMaxX = 0; int tMaxY = 0; int tMaxZ = 0; // only check for nearby voxels (ray length) int i = 0; while (i++ < 400) { double diffYX = valYX * (tMaxX + offX) - (tMaxY + offY); if (diffYX < 0) { double diffZX = valZX * (tMaxX + offX) - (tMaxZ + offZ); if (diffZX < 0) { tMaxX++; pos = CubeIndexer.changeX(pos, stepXB); lastHitSide = sideX; } else { tMaxZ++; pos = CubeIndexer.changeZ(pos, stepZB); lastHitSide = sideZ; } } else { double diffZY = valZY * (tMaxY + offY) - (tMaxZ + offZ); if (diffZY < 0) { tMaxY++; pos = CubeIndexer.changeY(pos, stepYB); lastHitSide = sideY; } else { tMaxZ++; pos = CubeIndexer.changeZ(pos, stepZB); lastHitSide = sideZ; } } // check for containment if (containsBorder(pos, lastHitSide)) { // hit side has to be visible //if (id2obj.containsKey(pos)) { // any voxel can be hit short[] result = CubeIndexer.getPos(pos); return new short[] {result[0], result[1], result[2], lastHitSide}; } } return null; } // constructor // ============================== // -- exterior computation // ============================== // holds the computed exterior private final TIntHashSet[] exterior = new TIntHashSet[] { new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet() }; // holds the computed interior private final TIntHashSet[] interior = new TIntHashSet[] { new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet() }; // helper, return true if given border is present in hull // if true -> add border to stack if not already present in processed private boolean detectStepAdd(ArrayList<int[]> stack, int pos, int orientation, TIntHashSet[] processed) { boolean result = false; // (1) check if extension exists as border, if (containsBorder(pos, orientation)) { // (2) check if extension already processed, if (!processed[orientation].contains(pos)) { processed[orientation].add(pos); // (3) add to stack stack.add(new int[] {pos, orientation}); } result = true; } return result; } // check which borders are correct neighbouring borders for a given border // then add to stack if not already processed // Note: This uses a "fold down model" for checking the neighbouring borders private void detectStep(ArrayList<int[]> stack, int pos, short orientation, TIntHashSet[] processed) { int[][] axisToCheck; switch (orientation) { case 0:case 1: axisToCheck = new int[][] { new int[] {2,3}, new int[] {4,5} }; break; case 2:case 3: axisToCheck = new int[][] { new int[] {0,1}, new int[] {4,5} }; break; default: axisToCheck = new int[][] { new int[] {0,1}, new int[] {2,3} }; break; } // -- for (int[] axis : axisToCheck) { // check negative int posN = CubeIndexer.change(pos, axis[0]); int posNOff = CubeIndexer.change(posN, orientation); boolean detectedN = detectStepAdd(stack, posNOff, axis[1], processed) || detectStepAdd(stack, posN, orientation, processed) || detectStepAdd(stack, pos, axis[0], processed); // check positive int posP = CubeIndexer.change(pos, axis[1]); int posPOff = CubeIndexer.change(posP, orientation); boolean detectedP = detectStepAdd(stack, posPOff, axis[0], processed) || detectStepAdd(stack, posP, orientation, processed) || detectStepAdd(stack, pos, axis[1], processed); // at least one neighbouring side has to be detected into each direction // otherwise this voxel face would be bugged (i.e. have a "see through" face // as a neighbour) assert detectedN && detectedP; } } // follow an outline for a given starting border // store found borders in processed // returns the detected outline private TIntHashSet[] detectContour(int pos, short orientation, TIntHashSet[] processed) { TIntHashSet[] result = new TIntHashSet[] { new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet() }; // stack of currently processing voxel sides ArrayList<int[]> stack = new ArrayList<int[]>(); detectStepAdd(stack, pos, orientation, processed); // follow all path while (!stack.isEmpty()) { int[] cur = stack.remove(0); // add to result result[cur[1]].add(cur[0]); // check all extensions and add to stack detectStep(stack, cur[0], (short) cur[1], processed); } // return the result return result; } // helper class that wraps a (continuous set of sides) private static final class HullWrapper { // holds the data private TIntHashSet[] data = new TIntHashSet[] { new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet() }; // constructor public HullWrapper(TIntHashSet[] data) { for (int i = 0; i < 6; i++) { this.data[i].addAll(data[i]); } } // returns true if containment is detected public boolean contains(HullWrapper other) { // -- check if the other HullWrapper is contained in this hull wrapper // fetch a first side into X direction short[] side = CubeIndexer.getPos(other.data[0].iterator().next()); // search for all sides at the found YZ position in the potential "outer" HullWrapper ArrayList<Short> list = new ArrayList<Short>(); for (TIntIterator it = this.data[0].iterator(); it.hasNext();) { short[] val = CubeIndexer.getPos(it.next()); if (val[1] == side[1] && val[2] == side[2]) { list.add(val[0]); } } for (TIntIterator it = this.data[1].iterator(); it.hasNext();) { short[] val = CubeIndexer.getPos(it.next()); if (val[1] == side[1] && val[2] == side[2]) { list.add(val[0]); } } // sort the found sides Collections.sort(list); // check if the initially selected side lives inside the extracted sides boolean inside = false; short lastDepth = 0; boolean foundInside = false; for (short depth : list) { inside = !inside; if (!inside) { if (side[0] < depth && side[0] > lastDepth) { foundInside = true; break; } } lastDepth = depth; } return foundInside; } } // compute the "outside" of the described object @Override public boolean computeExterior() { // holds known exterior sides ArrayList<HullWrapper> exterior = new ArrayList<HullWrapper>(); // holds known interior sides ArrayList<HullWrapper> interior = new ArrayList<HullWrapper>(); // holds the processed sides TIntHashSet[] processed = new TIntHashSet[]{ new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet(), new TIntHashSet() }; // true if a hole was found boolean interiorFound = false; // loop over all potential starting positions // (one direction is enough for this!) for (int pos : getHullAsIds(0)) { // check if this side was already processed with another starting position if (!processed[0].contains(pos)) { // -- fetch the contour that this border belongs to TIntHashSet[] detected = detectContour(pos, (short) 0, processed); // -- analyse whether it's outside or inside facing hull int minA = Integer.MAX_VALUE; for (TIntIterator it = detected[0].iterator(); it.hasNext(); ) { minA = Math.min(minA, CubeIndexer.getPos(it.next())[0]); } int minB = Integer.MAX_VALUE; for (TIntIterator it = detected[1].iterator(); it.hasNext(); ) { minB = Math.min(minB, CubeIndexer.getPos(it.next())[0]); } boolean isInsideHull = minA < minB; // -- add to according lists if (isInsideHull) { interiorFound = true; // add to interior list interior.add(new HullWrapper(detected)); } else { // add to exterior list exterior.add(new HullWrapper(detected)); } } } // check if we need to migrate any exterior to interior // (this is the case if the outside facing hull lives // inside an inside facing hull) int i = 0; while (i < exterior.size()) { HullWrapper exteriorWrapper = exterior.get(i); boolean found = false; for (HullWrapper interiorWrapper : interior) { if (interiorWrapper.contains(exteriorWrapper)) { found = true; break; } } if (found) { interior.add(exterior.remove(i)); } else { i++; } } // update global exterior and interior for (int k = 0; k < 6; k++) { this.exterior[k].clear(); for (HullWrapper wrapper : exterior) { this.exterior[k].addAll(wrapper.data[k]); } this.interior[k].clear(); for (HullWrapper wrapper : interior) { this.interior[k].addAll(wrapper.data[k]); } } // return result return interiorFound; } // fetch the "outside" faces of the described object // into a specific direction. // Required computeExterior() to be called before working @Override public short[][] getExteriorHull(int direction) { short[][] result = new short[exterior[direction].size()][3]; // allocate with correct size int count = 0; for (TIntIterator it = exterior[direction].iterator(); it.hasNext(); ) { short[] val = CubeIndexer.getPos(it.next()); result[count][0] = val[0]; result[count][1] = val[1]; result[count][2] = val[2]; count++; } return result; } // fetch the "inside" faces of the described object // into a specific direction. // Required computeExterior() to be called before working @Override public short[][] getInteriorHull(int direction) { short[][] result = new short[interior[direction].size()][3]; // allocate with correct size int count = 0; for (TIntIterator it = interior[direction].iterator(); it.hasNext(); ) { short[] val = CubeIndexer.getPos(it.next()); result[count][0] = val[0]; result[count][1] = val[1]; result[count][2] = val[2]; count++; } return result; } // comparator - order by depth private static final Comparator<short[]> comparator = new Comparator<short[]>() { @Override public int compare(short[] o1, short[] o2) { return o1[0] - o2[0]; } }; // get the empty positions of voxels inside // Required computeExterior() to be called before working @Override public int[] getEmptyInterior() { // result TIntArrayList list = new TIntArrayList(); // -- fetch the interior faces into two opposite directions short[][] hullA = getInteriorHull(0); short[][] hullB = getInteriorHull(1); if (hullA.length > 0 && hullB.length > 0) { // -- order by depth Arrays.sort(hullA, comparator); Arrays.sort(hullB, comparator); // find places to fill int iA = 0; int iB = 0; TIntShortHashMap buffer = new TIntShortHashMap(); while (iB < hullB.length || iA < hullA.length) { if (iA < hullA.length && (!(iB < hullB.length) || hullA[iA][0] < hullB[iB][0])) { // front face - add the starting position buffer.put(CubeIndexer.getId(0, hullA[iA][1], hullA[iA][2]), hullA[iA][0]); iA++; } else { // back face - add missing until finish positions short val = buffer.remove(CubeIndexer.getId(0, hullB[iB][1], hullB[iB][2])); for (short pos = ++val; pos < hullB[iB][0]; pos++) { list.add(CubeIndexer.getId(pos, hullB[iB][1], hullB[iB][2])); } iB++; } } } // empty result return list.toArray(); } // get the voxel positions of voxels inside // Required computeExterior() to be called before working @Override public int[] getFilledInterior() { TIntArrayList result = new TIntArrayList(); // loop over all objects for (int posId : getPosIds()) { // exclude positions that have an exterior face attached if (!exterior[0].contains(posId) && !exterior[1].contains(posId) && !exterior[2].contains(posId) && !exterior[3].contains(posId) && !exterior[4].contains(posId) && !exterior[5].contains(posId)) { result.add(posId); } } // return result return result.toArray(); } }