diff --git a/applications/src/main/java/boofcv/app/MeshViewerApp.java b/applications/src/main/java/boofcv/app/MeshViewerApp.java index 5ab17e29a5..565c6e43bd 100644 --- a/applications/src/main/java/boofcv/app/MeshViewerApp.java +++ b/applications/src/main/java/boofcv/app/MeshViewerApp.java @@ -72,23 +72,39 @@ private static void loadFile( File file ) { } // See if there should be a texture mapped file - InterleavedU8 rgb = null; - if (mesh.texture.size() > 0) { + InterleavedU8 textureImage = null; + escape:if (mesh.texture.size() > 0) { System.out.println("Loading texture image"); String name = FilenameUtils.getBaseName(file.getName()); - File textureFile = new File(file.getParentFile(), name + ".jpg"); - rgb = UtilImageIO.loadImage(textureFile, true, ImageType.IL_U8); - if (rgb == null) - System.err.println("Failed to load texture image"); + // try to load an image with the same basename + File[] children = file.getParentFile().listFiles(); + if (children == null) { + break escape; + } + + for (File child : children) { + // see if this file starts with the same name as the mesh file + if (!child.getName().startsWith(name)) + continue; + + // skip if it's not an image + if (!UtilImageIO.isImage(child)) + continue; + + textureImage = UtilImageIO.loadImage(child, true, ImageType.IL_U8); + if (textureImage != null) + break; + } } - InterleavedU8 _rgb = rgb; + + InterleavedU8 _image = textureImage; SwingUtilities.invokeLater(() -> { var panel = new MeshViewerPanel(); panel.setMesh(mesh, false); if (colors.size > 0) panel.setVertexColors("RGB", colors.data); - if (_rgb != null) - panel.setTextureImage(_rgb); + if (_image != null) + panel.setTextureImage(_image); panel.setPreferredSize(new Dimension(500, 500)); ShowImages.showWindow(panel, "Mesh Viewer", true); }); diff --git a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/MeshViewerPanel.java b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/MeshViewerPanel.java index da983d349f..4db79689a4 100644 --- a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/MeshViewerPanel.java +++ b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/MeshViewerPanel.java @@ -281,7 +281,7 @@ private void renderLoop() { } // Stop it from sucking up all the CPU - Thread.yield(); + BoofMiscOps.sleep(10); // Can't sleep or wait here. That requires interrupting the thread to wake it up, unfortunately // that conflicts with the concurrency code and interrupts that too @@ -374,17 +374,31 @@ public void setHorizontalFov( double degrees ) { * Each time this is called it will change the colorizer being used, if more than one has been specified */ public void cycleColorizer() { + int totalColors = colorizers.size(); + if (mesh.isTextured()) { + totalColors++; + } + synchronized (colorizers) { var list = new ArrayList<>(colorizers.keySet()); // go to the next one and make sure it's valid activeColorizer++; - if (activeColorizer >= list.size()) { + if (activeColorizer >= totalColors) { activeColorizer = 0; } - // Change the colorizer - renderer.surfaceColor = Objects.requireNonNull(colorizers.get(list.get(activeColorizer))); + // If it has texture then that is the first possible colorizer + if (mesh.isTextured()) { + if (activeColorizer == 0) { + renderer.forceColorizer = false; + } else { + renderer.forceColorizer = true; + renderer.surfaceColor = Objects.requireNonNull(colorizers.get(list.get(activeColorizer - 1))); + } + } else { + renderer.surfaceColor = Objects.requireNonNull(colorizers.get(list.get(activeColorizer))); + } // Re-render the image requestRender(); diff --git a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPoint.java b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPoint.java index 19591bdc11..e64584dbe0 100644 --- a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPoint.java +++ b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPoint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Peter Abeles. All Rights Reserved. + * Copyright (c) 2024, Peter Abeles. All Rights Reserved. * * This file is part of BoofCV (http://boofcv.org). * @@ -21,16 +21,12 @@ import boofcv.alg.geo.PerspectiveOps; import boofcv.struct.calib.CameraPinhole; import georegression.geometry.ConvertRotation3D_F64; -import georegression.geometry.GeometryMath_F64; import georegression.metric.UtilAngle; import georegression.struct.EulerType; import georegression.struct.point.Point2D_F64; import georegression.struct.point.Point3D_F64; -import georegression.struct.point.Vector3D_F64; import georegression.struct.se.Se3_F64; import lombok.Getter; -import org.ejml.data.DMatrixRMaj; -import org.ejml.dense.row.CommonOps_DDRM; /** * Contains the mathematics for controlling a camera by orbiting around a point in 3D space @@ -44,54 +40,73 @@ public class OrbitAroundPoint { /** Transform from world to camera view reference frames */ Se3_F64 worldToView = new Se3_F64(); - DMatrixRMaj localRotation = new DMatrixRMaj(3, 3); - DMatrixRMaj rotationAroundTarget = new DMatrixRMaj(3, 3); - DMatrixRMaj tmp = new DMatrixRMaj(3, 3); - - // Translation applied after the orbit has been done - Vector3D_F64 translateWorld = new Vector3D_F64(); - - // Point it's orbiting around - Point3D_F64 targetPoint = new Point3D_F64(); - - // Adjustment applied to distance from target point in final transform - double radiusScale = 1.0; - - Point3D_F64 cameraLoc = new Point3D_F64(); + // Point it's orbiting around in world coordinates + private Point3D_F64 targetWorld = new Point3D_F64(); // Storage for normalized image coordinates Point2D_F64 norm1 = new Point2D_F64(); Point2D_F64 norm2 = new Point2D_F64(); + Point3D_F64 targetInView = new Point3D_F64(); + Point3D_F64 targetInOld = new Point3D_F64(); + Se3_F64 viewToPoint = new Se3_F64(); + Se3_F64 viewToPoint2 = new Se3_F64(); + Se3_F64 worldToPoint = new Se3_F64(); + Se3_F64 pointToRot = new Se3_F64(); + Se3_F64 oldToNew = new Se3_F64(); + public OrbitAroundPoint() { resetView(); } + public void setTarget( double x, double y, double z ) { + targetWorld.setTo(x, y, z); + + updateAfterExternalChange(); + } + public void resetView() { - radiusScale = 1.0; - translateWorld.zero(); - CommonOps_DDRM.setIdentity(rotationAroundTarget); + worldToView.reset(); + + // See if the principle point and the target are on top of each other + updateAfterExternalChange(); + } + + /** + * After an external change to the transform or target, make sure it's pointed at the target and a reasonable + * distance. + */ + private void updateAfterExternalChange() { + // See if the principle point and the target are on top of each other + if (Math.abs(worldToView.T.distance(targetWorld)) < 1e-16) { + // back the camera off a little bit + worldToView.T.z += 0.1; + } + + pointAtTarget(); } - public void updateTransform() { - // Compute location of camera principle point with no rotation in target point reference frame - cameraLoc.x = -targetPoint.x*radiusScale; - cameraLoc.y = -targetPoint.y*radiusScale; - cameraLoc.z = -targetPoint.z*radiusScale; - - // Apply rotation - GeometryMath_F64.mult(rotationAroundTarget, cameraLoc, cameraLoc); - - // Compute the full transform - worldToView.T.setTo( - cameraLoc.x + targetPoint.x + translateWorld.x, - cameraLoc.y + targetPoint.y + translateWorld.y, - cameraLoc.z + targetPoint.z + translateWorld.z); - worldToView.R.setTo(rotationAroundTarget); + /** + * Points the camera at the target point + */ + private void pointAtTarget() { + oldToNew.reset(); + + worldToView.transform(targetWorld, targetInView); + PerspectiveOps.pointAt(targetInView.x, targetInView.y, targetInView.z, oldToNew.R); + + Se3_F64 tmp = pointToRot; + worldToView.concatInvert(oldToNew, tmp); + worldToView.setTo(tmp); } public void mouseWheel( double ticks, double scale ) { - radiusScale = Math.max(0.005, radiusScale*(1.0 + 0.02*ticks*scale)); + worldToView.transform(targetWorld, targetInView); + + // How much it will move towards or away from the target + worldToView.T.z += targetInView.norm()*0.02*ticks*scale; + + // Because it's moving towards the point it won't need to adjust its angle } public void mouseDragRotate( double x0, double y0, double x1, double y1 ) { @@ -107,31 +122,29 @@ public void mouseDragRotate( double x0, double y0, double x1, double y1 ) { double rotX = UtilAngle.minus(Math.atan(norm1.x), Math.atan(norm2.x)); double rotY = UtilAngle.minus(Math.atan(norm1.y), Math.atan(norm2.y)); - // Set the local rotation - ConvertRotation3D_F64.eulerToMatrix(EulerType.XYZ, -rotY, rotX, 0, localRotation); - - // Update the global rotation - CommonOps_DDRM.mult(localRotation, rotationAroundTarget, tmp); - rotationAroundTarget.setTo(tmp); + applyLocalEuler(rotX, -rotY, 0.0); } /** - * Uses mouse drag motion to translate the view + * Applies a X Y Z Euler rotation in the point's reference frame so that it will appear to be rotating + * around that point */ - public void mouseDragTranslate( double x0, double y0, double x1, double y1 ) { - // do nothing if the camera isn't configured yet - if (camera.fx == 0.0 || camera.fy == 0.0) - return; + private void applyLocalEuler( double rotX, double rotY, double rotZ ) { + pointToRot.reset(); - // convert into normalize image coordinates - PerspectiveOps.convertPixelToNorm(camera, x0, y0, norm1); - PerspectiveOps.convertPixelToNorm(camera, x1, y1, norm2); + worldToView.transform(targetWorld, targetInOld); + + viewToPoint.T.setTo(-targetInOld.x, -targetInOld.y, -targetInOld.z); - // Figure out the distance along the projection at the plane at the distance of the target point - double z = targetPoint.plus(translateWorld).norm()*radiusScale; + worldToView.concat(viewToPoint, worldToPoint); - translateWorld.x += (norm2.x - norm1.x)*z; - translateWorld.y += (norm2.y - norm1.y)*z; + // Set the local rotation + ConvertRotation3D_F64.eulerToMatrix(EulerType.XYZ, rotY, rotX, rotZ, pointToRot.R); + + viewToPoint.concat(pointToRot, viewToPoint2); + worldToPoint.concatInvert(viewToPoint2, worldToView); + + pointAtTarget(); } /** @@ -147,14 +160,11 @@ public void mouseDragZoomRoll( double x0, double y0, double x1, double y1 ) { PerspectiveOps.convertPixelToNorm(camera, x1, y1, norm2); // Zoom in and out using the mouse - double z = targetPoint.plus(translateWorld).norm()*radiusScale; - translateWorld.z += (norm2.y - norm1.y)*z; + worldToView.transform(targetWorld, targetInView); + worldToView.T.z += (norm2.y - norm1.y)*targetInView.norm(); - // Perform roll around the z-axis + // Roll the camera double rotX = UtilAngle.minus(Math.atan(norm1.x), Math.atan(norm2.x)); - ConvertRotation3D_F64.eulerToMatrix(EulerType.XYZ, 0, 0, -rotX, localRotation); - CommonOps_DDRM.mult(localRotation, rotationAroundTarget, tmp); - rotationAroundTarget.setTo(tmp); - + applyLocalEuler(0, 0, rotX); } } diff --git a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPointControl.java b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPointControl.java index d39e74c092..e345c02a4a 100644 --- a/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPointControl.java +++ b/integration/boofcv-swing/src/main/java/boofcv/gui/mesh/OrbitAroundPointControl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Peter Abeles. All Rights Reserved. + * Copyright (c) 2024, Peter Abeles. All Rights Reserved. * * This file is part of BoofCV (http://boofcv.org). * @@ -76,7 +76,7 @@ public class OrbitAroundPointControl extends MouseAdapter implements Swing3dCame // Give tell it to look in front of the camera if there is nothing to look at if (N == 0) { - orbit.targetPoint.setTo(0, 0, 1); + orbit.setTarget(0, 0, 1); return; } @@ -94,12 +94,14 @@ public class OrbitAroundPointControl extends MouseAdapter implements Swing3dCame values.resize(sampled.size); synchronized (orbit) { + var target = new Point3D_F64(); for (int axis = 0; axis < 3; axis++) { int _axis = axis; sampled.forIdx(( idx, v ) -> values.set(idx, v.getIdx(_axis))); values.sort(); - orbit.targetPoint.setIdx(axis, values.getFraction(0.5)); + target.setIdx(axis, values.getFraction(0.5)); } + orbit.setTarget(target.x, target.y, target.z); } } @@ -111,7 +113,6 @@ public class OrbitAroundPointControl extends MouseAdapter implements Swing3dCame @Override public Se3_F64 getWorldToCamera() { synchronized (orbit) { - orbit.updateTransform(); return orbit.worldToView.copy(); } } @@ -134,9 +135,7 @@ public class OrbitAroundPointControl extends MouseAdapter implements Swing3dCame @Override public void mouseDragged( MouseEvent e ) { synchronized (orbit) { - if (e.isShiftDown() || SwingUtilities.isMiddleMouseButton(e)) - orbit.mouseDragTranslate(prevX, prevY, e.getX(), e.getY()); - else if (e.isControlDown() || SwingUtilities.isRightMouseButton(e)) + if (e.isControlDown() || SwingUtilities.isRightMouseButton(e)) orbit.mouseDragZoomRoll(prevX, prevY, e.getX(), e.getY()); else orbit.mouseDragRotate(prevX, prevY, e.getX(), e.getY()); diff --git a/main/boofcv-geo/src/main/java/boofcv/alg/geo/PerspectiveOps.java b/main/boofcv-geo/src/main/java/boofcv/alg/geo/PerspectiveOps.java index 3d3e9a96f9..8a6d597090 100644 --- a/main/boofcv-geo/src/main/java/boofcv/alg/geo/PerspectiveOps.java +++ b/main/boofcv-geo/src/main/java/boofcv/alg/geo/PerspectiveOps.java @@ -1178,19 +1178,27 @@ public static DMatrixRMaj pointAt( double x, double y, double z, @Nullable DMatr // There's a pathological case. Pick the option which if farthest from it if (Math.abs(GeometryMath_F64.dot(axisX, axisZ)) < Math.abs(GeometryMath_F64.dot(axisY, axisZ))) { GeometryMath_F64.cross(axisX, axisZ, axisY); + // There are two options here, pick the one that will result in the smallest rotation + if (dot(axisY,0,1,0) < 0) + axisY.scale(-1); + axisY.divideIP(axisY.norm()); GeometryMath_F64.cross(axisY, axisZ, axisX); axisX.divideIP(axisX.norm()); } else { GeometryMath_F64.cross(axisY, axisZ, axisX); axisX.divideIP(axisX.norm()); - GeometryMath_F64.cross(axisX, axisZ, axisY); + if (dot(axisX,1,0,0) < 0) + axisX.scale(-1); + + GeometryMath_F64.cross(axisZ, axisX, axisY); axisY.divideIP(axisY.norm()); } + if (R == null) - R = new DMatrixRMaj(3,3); - + R = new DMatrixRMaj(3, 3); + R.set(0, 0, axisX.x); R.set(1, 0, axisX.y); R.set(2, 0, axisX.z); @@ -1203,4 +1211,8 @@ public static DMatrixRMaj pointAt( double x, double y, double z, @Nullable DMatr return R; } + + private static double dot(Point3D_F64 p, double x, double y , double z) { + return p.x*x + p.y*y + p.z*z; + } } diff --git a/main/boofcv-geo/src/test/java/boofcv/alg/geo/TestPerspectiveOps.java b/main/boofcv-geo/src/test/java/boofcv/alg/geo/TestPerspectiveOps.java index 6a2a6ccc00..34c0799a81 100644 --- a/main/boofcv-geo/src/test/java/boofcv/alg/geo/TestPerspectiveOps.java +++ b/main/boofcv-geo/src/test/java/boofcv/alg/geo/TestPerspectiveOps.java @@ -861,4 +861,28 @@ void checkBehindSwapSign( Point4D_F64 p, boolean expected ) { assertTrue(point.z > 0); } } + + /** + * In these scenarios the rotation matrix should have all positive elements along the diagonal since there + * should be no flipping + */ + @Test void pointAt_positive_diagonal() { + var points = new ArrayList(); + + points.add(new Point3D_F64(-1.3877787807814457E-17, 1.3877787807814457E-17, 3.344561447900182)); + points.add(new Point3D_F64(-1.3877787807814457E-17, 0.0, 3.344561447900182)); + points.add(new Point3D_F64(9.540979117872439E-17, -3.469446951953614E-18, 3.3445614479001797)); + points.add(new Point3D_F64(0.0, 5.551115123125783E-17, 3.3445614479001833)); + points.add(new Point3D_F64(0.0, -2.7755575615628914E-17, 3.3445614479001833)); + + var R = new DMatrixRMaj(3, 3); + + for (var point : points) { + PerspectiveOps.pointAt(point.x, point.y, point.z, R); + + for (int i = 0; i < 3; i++) { + assertTrue(R.get(i,i) > 0); + } + } + } } diff --git a/main/boofcv-io/src/main/java/boofcv/visualize/RenderMesh.java b/main/boofcv-io/src/main/java/boofcv/visualize/RenderMesh.java index 82eb731b6c..6fd35dd42d 100644 --- a/main/boofcv-io/src/main/java/boofcv/visualize/RenderMesh.java +++ b/main/boofcv-io/src/main/java/boofcv/visualize/RenderMesh.java @@ -79,7 +79,10 @@ public class RenderMesh implements VerbosePrint { public @Getter final Se3_F64 worldToView = new Se3_F64(); /** If true then a polygon will only be rendered if the surface normal is pointed towards the camera */ - public @Getter @Setter boolean checkSurfaceNormal = true; + public @Getter @Setter boolean checkSurfaceNormal = false; + + /** If true it will always use the colorizer, even if there is texture information */ + public @Getter @Setter boolean forceColorizer = false; // Image for texture mapping private InterleavedU8 textureImage = new InterleavedU8(1, 1, 3); @@ -126,8 +129,6 @@ public void render( VertexMesh mesh ) { // Initialize output images initializeImages(); - final int width = intrinsics.width; - final int height = intrinsics.height; final double fx = intrinsics.fx; final double fy = intrinsics.fy; final double cx = intrinsics.cx; @@ -139,6 +140,9 @@ public void render( VertexMesh mesh ) { var worldCamera = new Point3D_F64(); worldToView.transformReverse(worldCamera, worldCamera); + // Decide if it should texture map or user a per shape color + final boolean useColorizer = forceColorizer || mesh.texture.size() == 0; + for (int shapeIdx = 1; shapeIdx < mesh.offsets.size; shapeIdx++) { // First and last point in the polygon final int idx0 = mesh.offsets.get(shapeIdx - 1); @@ -152,13 +156,13 @@ public void render( VertexMesh mesh ) { polygonProj.vertexes.reset().reserve(idx1 - idx0); meshCam.reset().reserve(idx1 - idx0); - if (mesh.texture.size() > 0) { + if (!useColorizer) { mesh.getTexture(shapeIdx - 1, polygonTex.vertexes); } // Prune using normal vector if (mesh.normals.size() > 0 && checkSurfaceNormal) { - if (!isFrontVisible(mesh, shapeIdx, idx0, worldCamera)) continue; + if (!isFrontVisible(mesh, shapeIdx - 1, idx0, worldCamera)) continue; } boolean behindCamera = false; @@ -189,8 +193,8 @@ public void render( VertexMesh mesh ) { if (behindCamera) continue; - if (mesh.texture.size() == 0) { - projectSurfaceColor(mesh, polygonProj, shapeIdx - 1); + if (useColorizer) { + projectSurfaceColor(meshCam, polygonProj, shapeIdx - 1); } else { projectSurfaceTexture(meshCam, polygonProj, polygonTex); } @@ -204,11 +208,12 @@ public void render( VertexMesh mesh ) { /** * Use the normal vector to see if the front of the mesh is visible. If it's not visible we can skip it * + * @param worldCamera Location of the camera in current view in world coordinates * @return true if visible */ - private static boolean isFrontVisible( VertexMesh mesh, int shapeIdx, int idx0, Point3D_F64 worldCamera ) { + static boolean isFrontVisible( VertexMesh mesh, int shapeIdx, int idx0, Point3D_F64 worldCamera ) { // Get normal in world coordinates - Point3D_F64 normal = mesh.normals.getTemp(shapeIdx - 1); + Point3D_F64 normal = mesh.normals.getTemp(shapeIdx); // vector from the camera to a vertex Point3D_F64 v1 = mesh.vertexes.getTemp(mesh.indexes.get(idx0)); @@ -253,18 +258,9 @@ static void computeBoundingBox( int width, int height, Polygon2D_F64 polygon, Re * is searched exhaustively. If the projected 2D polygon contains a pixels and the polygon is closer than * the current depth of the pixel it is rendered there and the depth image is updated. */ - void projectSurfaceColor( VertexMesh mesh, Polygon2D_F64 polyProj, int shapeIdx ) { - // TODO temp hack. Best way is to find the distance to the 3D polygon at this point. Instead we will - // use the depth of the first point. - // - // IDEA: Use a homography to map location on 2D polygon to 3D polygon, then rotate just the Z to get - // local depth on the surface. - int vertexIndex = mesh.indexes.get(mesh.offsets.data[shapeIdx]); - Point3D_F64 world = mesh.vertexes.getTemp(vertexIndex); - worldToView.transform(world, camera); - + void projectSurfaceColor( FastAccess mesh, Polygon2D_F64 polyProj, int shapeIdx ) { // TODO compute the depth at each pixel - float depth = (float)camera.z; + float depth = (float)mesh.get(0).z; // TODO look at vertexes and get min/max depth. Use that to quickly reject pixels based on depth without // convex intersection or computing the depth at that pixel on this surface @@ -272,7 +268,7 @@ void projectSurfaceColor( VertexMesh mesh, Polygon2D_F64 polyProj, int shapeIdx // The entire surface will have one color int color = surfaceColor.surfaceRgb(shapeIdx); - computeBoundingBox(intrinsics.width, intrinsics.height, polygonProj, aabb); + computeBoundingBox(intrinsics.width, intrinsics.height, polyProj, aabb); // Go through all pixels and see if the points are inside the polygon. If so for (int pixelY = aabb.y0; pixelY < aabb.y1; pixelY++) { @@ -299,7 +295,7 @@ void projectSurfaceColor( VertexMesh mesh, Polygon2D_F64 polyProj, int shapeIdx * Projection with texture mapping. Breaks the polygon up into triangles and uses Barycentric coordinates to * map pixels to textured mapped coordinates. * - * @param mesh 3D location of vertexes in the mesh + * @param mesh 3D location of vertexes in the mesh in the view's coordinate system * @param polyProj Projected pixels of mesh * @param polyText Texture coordinates of the mesh */ @@ -394,7 +390,7 @@ void projectSurfaceTexture( FastAccess mesh, Polygon2D_F64 polyProj /** * Gets the RGB color using interpolation at the specified pixel coordinate in the texture image */ - private int interpolateTextureRgb( float px, float py ) { + int interpolateTextureRgb( float px, float py ) { textureInterp.get(px, py, textureValues); int r = (int)(textureValues[0] + 0.5f); int g = (int)(textureValues[1] + 0.5f); diff --git a/main/boofcv-io/src/test/java/boofcv/visualize/TestRenderMesh.java b/main/boofcv-io/src/test/java/boofcv/visualize/TestRenderMesh.java index 8de657a640..cb2068b971 100644 --- a/main/boofcv-io/src/test/java/boofcv/visualize/TestRenderMesh.java +++ b/main/boofcv-io/src/test/java/boofcv/visualize/TestRenderMesh.java @@ -21,8 +21,12 @@ import boofcv.alg.geo.PerspectiveOps; import boofcv.struct.mesh.VertexMesh; import boofcv.testing.BoofStandardJUnit; +import georegression.struct.point.Point2D_F64; +import georegression.struct.point.Point3D_F64; +import georegression.struct.shapes.Polygon2D_F32; import georegression.struct.shapes.Polygon2D_F64; import georegression.struct.shapes.Rectangle2D_I32; +import org.ddogleg.struct.DogArray; import org.ddogleg.struct.DogArray_I32; import org.junit.jupiter.api.Test; @@ -44,6 +48,9 @@ public class TestRenderMesh extends BoofStandardJUnit { // Configure var alg = new RenderMesh(); + + // turn off checking with normals to simply this test + alg.setCheckSurfaceNormal(false); PerspectiveOps.createIntrinsic(300, 200, 90, -1, alg.intrinsics); // Render @@ -53,7 +60,7 @@ public class TestRenderMesh extends BoofStandardJUnit { int count = 0; for (int y = 0; y < alg.intrinsics.height; y++) { for (int x = 0; x < alg.intrinsics.width; x++) { - if (alg.rgbImage.get24(x,y) != 0xFFFFFF) + if (alg.rgbImage.get24(x, y) != 0xFFFFFF) count++; } } @@ -86,14 +93,12 @@ public class TestRenderMesh extends BoofStandardJUnit { } /** - * Tests the projection by having it fill in a known rectangle. The AABB is larger than needed. One pixel - * is given a depth closer than the polygon and isn't filled in. + * Tests the projection by having it fill in a known rectangle. */ @Test void projectSurfaceColor() { var alg = new RenderMesh(); alg.intrinsics.fsetShape(100, 120); alg.initializeImages(); - alg.aabb.setTo(10, 15, 50, 60); // Polygon of projected shape on to the image. Make is an AABB, but smaller than the one above var polygon = new Polygon2D_F64(); @@ -102,35 +107,109 @@ public class TestRenderMesh extends BoofStandardJUnit { polygon.vertexes.grow().setTo(40, 35); polygon.vertexes.grow().setTo(10, 35); - // The mesh is used to get the depth of the shape being examined - var mesh = new VertexMesh(); - mesh.vertexes.append(0, 0, 10); - mesh.indexes.add(0); - mesh.offsets.add(1); - - // Set one pixel inside the projected region to be closer than the mesh - alg.depthImage.set(15, 25, 1); + var shapeInCamera = new DogArray<>(Point3D_F64::new); + shapeInCamera.resize(polygon.size()); + shapeInCamera.get(0).setTo(0, 0, 10); // the depth will be 10 for the shape // Perform the projection - alg.projectSurfaceColor(mesh, polygon, 0); + alg.projectSurfaceColor(shapeInCamera, polygon, 0); // Verify by counting the number of projected points int countDepth = 0; int countRgb = 0; for (int y = 0; y < alg.intrinsics.height; y++) { for (int x = 0; x < alg.intrinsics.width; x++) { - if (alg.depthImage.get(x,y) == 10) + if (alg.depthImage.get(x, y) == 10) countDepth++; - if (alg.rgbImage.get24(x,y) != 0xFFFFFF) + if (alg.rgbImage.get24(x, y) != 0xFFFFFF) countRgb++; } } - assertEquals(599, countDepth); - assertEquals(599, countRgb); + assertEquals(600, countDepth); + assertEquals(600, countRgb); } @Test void projectSurfaceTexture() { - fail("implement"); + var alg = new RenderMesh() { + @Override int interpolateTextureRgb( float px, float py ) { + // return some arbitrary color + return 1; + } + }; + alg.intrinsics.fsetShape(100, 120); + alg.initializeImages(); + + // Polygon of projected shape on to the image. Make is an AABB, but smaller than the one above + var polygon = new Polygon2D_F64(); + polygon.vertexes.grow().setTo(10, 15); + polygon.vertexes.grow().setTo(40, 15); + polygon.vertexes.grow().setTo(40, 35); + polygon.vertexes.grow().setTo(10, 35); + + var shapeInCamera = new DogArray<>(Point3D_F64::new); + shapeInCamera.resize(polygon.size(), ( p ) -> p.setTo(0, 0, 10)); + // All points will have a depth of 10 + + // Create texture with reasonable coordinates. Doesn't really matter what they are + var polyTexture = new Polygon2D_F32(); + for (int i = 0; i < polygon.size(); i++) { + Point2D_F64 p = polygon.get(i); + polyTexture.vertexes.grow().setTo((float)p.x/50, (float)p.y/50); + } + + // Perform the projection + alg.projectSurfaceTexture(shapeInCamera, polygon, polyTexture); + + // Verify by counting the number of projected points + int countDepth = 0; + int countRgb = 0; + for (int y = 0; y < alg.intrinsics.height; y++) { + for (int x = 0; x < alg.intrinsics.width; x++) { + if (!Float.isNaN(alg.depthImage.get(x, y))) { + countDepth++; + } + if (alg.rgbImage.get24(x, y) != 0xFFFFFF) + countRgb++; + } + } + + assertEquals(600, countDepth); + assertEquals(600, countRgb); + } + + /** + * Rotate in a circle and check two handcrafted scenarios + */ + @Test void isFrontVisible() { + var mesh = new VertexMesh(); + + double r = 5; + + var pointCam = new Point3D_F64(0, 2, 2); + + // Should pass all of these + for (int i = 0; i < 30; i++) { + double yaw = Math.PI*i/15; + + double c = Math.cos(yaw); + double s = Math.sin(yaw); + + // This should pass + mesh.reset(); + mesh.indexes.add(0); + mesh.normals.append(-c, -s, 0); + mesh.vertexes.append(r*c, 2 + r*s, 2); + + assertTrue(RenderMesh.isFrontVisible(mesh, 0, 0, pointCam)); + + // This should fail + mesh.reset(); + mesh.indexes.add(0); + mesh.normals.append(c, s, 0); + mesh.vertexes.append(r*c, 2 + r*s, 2); + + assertFalse(RenderMesh.isFrontVisible(mesh, 0, 0, pointCam)); + } } } diff --git a/main/boofcv-types/src/main/java/boofcv/struct/mesh/VertexMesh.java b/main/boofcv-types/src/main/java/boofcv/struct/mesh/VertexMesh.java index 44e9ed8e79..92dcd61464 100644 --- a/main/boofcv-types/src/main/java/boofcv/struct/mesh/VertexMesh.java +++ b/main/boofcv-types/src/main/java/boofcv/struct/mesh/VertexMesh.java @@ -184,6 +184,7 @@ public void reset() { indexes.reset(); offsets.reset(); texture.reset(); + normals.reset(); offsets.add(0); } @@ -201,4 +202,8 @@ public MeshPolygonAccess toAccess() { } }; } + + public boolean isTextured() { + return texture.size() > 0; + } } diff --git a/main/boofcv-types/src/test/java/boofcv/struct/mesh/TestVertexMesh.java b/main/boofcv-types/src/test/java/boofcv/struct/mesh/TestVertexMesh.java index 74777edc34..1866fe98cc 100644 --- a/main/boofcv-types/src/test/java/boofcv/struct/mesh/TestVertexMesh.java +++ b/main/boofcv-types/src/test/java/boofcv/struct/mesh/TestVertexMesh.java @@ -19,15 +19,17 @@ package boofcv.struct.mesh; import boofcv.testing.BoofStandardJUnit; +import georegression.struct.point.Point2D_F32; import georegression.struct.point.Point3D_F64; import org.ddogleg.struct.DogArray; +import org.ejml.UtilEjml; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; public class TestVertexMesh extends BoofStandardJUnit { @Test void setTo() {checkSetTo(VertexMesh.class, true);} @@ -54,11 +56,55 @@ public class TestVertexMesh extends BoofStandardJUnit { } @Test void getTexture() { - fail("implement"); + var shape = new DogArray<>(Point3D_F64::new); + + var alg = new VertexMesh(); + alg.addShape(shape.resize(3).toList()); + alg.addShape(shape.resize(4).toList()); + alg.addShape(shape.resize(5).toList()); + + alg.addTexture(3, new float[]{1,2,3,4,5,6,7,8,9,10}); + alg.addTexture(4, new float[12]); + alg.addTexture(5, new float[10]); + + var found = new DogArray<>(Point2D_F32::new); + alg.getTexture(0, found); + assertEquals(3, found.size); + assertTrue(found.get(0).isIdentical(1,2)); + assertTrue(found.get(1).isIdentical(3,4)); + assertTrue(found.get(2).isIdentical(5,6)); + alg.getTexture(1, found); + assertEquals(4, found.size); + alg.getTexture(2, found); + assertEquals(5, found.size); } @Test void computeNormals() { - fail("implement"); + var shape = new DogArray<>(Point3D_F64::new); + + shape.grow().setTo(0,0,0); + shape.grow().setTo(0,1,0); + shape.grow().setTo(1,1,0); + + // create triangles that will have normals +1 and -1 + var alg = new VertexMesh(); + alg.addShape(shape.toList()); + shape.reverse(); + alg.addShape(shape.toList()); + + alg.computeNormals(); + assertEquals(0.0, alg.normals.getTemp(0).distance(0,0,-1), UtilEjml.TEST_F64); + assertEquals(0.0, alg.normals.getTemp(1).distance(0,0,1), UtilEjml.TEST_F64); + + // try it with a non-triangle + shape.reset(); + shape.grow().setTo(0,0,0); + shape.grow().setTo(0,2,0); + shape.grow().setTo(2,2,0); + shape.grow().setTo(2,9,0); + alg.reset(); + alg.addShape(shape.toList()); + assertEquals(0.0, alg.normals.getTemp(0).distance(0,0,-1), UtilEjml.TEST_F64); } private List createRandomShape( int count ) {