Is there any way to crop a 3d model and also export it

What im looking to do is crop the mesh based on the bounding box set by the user
I tired using boolean operations but the results are not as expected, parts of the mesh where there is nothing gets added geometry.

I got expected results by using clipping planes but that does not actually change the geometery of the mesh so cant export the model
Any insights will be helpful

I would have used boolean operations for that, what kind of unexpected geometry are you seeing? Can you share a Playground with it?

the models created are from photogrammetry so they are non manifold meshes, hence the boolean does not give ideal results
Hope this clarifies what I mean https://www.babylonjs-playground.com/#LXZPJK#1708
It adds extra geometry when there was nothing there int he initial mesh

I see, thanks for the additional context. Makes sense that CSG won’t work as well on this scenario. I think you’ll need a custom algorithm for that, and some kind of data structure to hold vertex connectivity, such as a half-edge: Half-Edge Data Structures (jerryyin.info)

I’d try something like this: for each triangle of the mesh, test if its three vertices are inside or outside the cutting box. We have three cases to consider:

  1. The three vertices are inside the box. We keep this triangle.
  2. The three vertices are outside the box. We discard this triangle.
  3. It has vertices inside and vertices outside the box. This is the more complicated case. It would look something like this:
    image
    Where the two orange vertices are outside of the box and the red vertex is inside. In that case, you’ll have to keep the red vertex, but exclude the orange vertices. There would be more than one way to cut up the geometry, but one approach could be to compute the intersections of the crossing edges with the cube, adding two new points in each intersection position, and forming a new triangle with the red vertex:
    image
    Then you could keep this newly created triangle and exclude the existing one.
2 Likes

Hey thanks for the explanation, I am already doing something like this in the backend while using bjs in frontend to help users with visualization, Most meshes are very high density so I wanted to avoid this as it will hinder the performance a lot on mobile devices and was looking if someone has done something out of the box magical voodoo that I can use.
Maybe in the future mobile devices will be powerful enough to support this kind of stuff without impacting user experience

Hey I ended up implementing this in babylonjs, however, I’m having one issue, the cropping works perfectly when done without any transformations on the mesh but if I rotate, move etc (change the transformation) it does weird stuff to the mesh, I am applying the transformation by bakeCurrentTransformIntoVertices to mesh and all its children which are meshes

import {
    AbstractMesh,
    Mesh,
    Scene,
    VertexData,
    Vector3,
    Vector2,
    VertexBuffer,
    Plane,
    Matrix,
} from '@babylonjs/core';

export function removeVerticesOutsideCube(
    abstractMesh: AbstractMesh,
    cubeMesh: Mesh,
    scene: Scene
): void {
    let mesh: Mesh | undefined;
    var meshParent = abstractMesh as Mesh;
    // console.log(meshParent.rotation)
    // meshParent.bakeCurrentTransformIntoVertices();
    // console.log(meshParent.rotation)
    abstractMesh.getChildMeshes(false).forEach((childMesh) => {
        if (childMesh instanceof Mesh) {
            mesh = childMesh as Mesh;
        }
    });

    if (!mesh) {
        console.error('No child mesh found in the provided abstract mesh.');
        return;
    }

    const parentMatrix = mesh.computeWorldMatrix(true);
    const vertexData = VertexData.ExtractFromMesh(mesh);
    const vertexPositions = vertexData.positions;

    if (!vertexPositions) {
        console.error('No vertex positions found in the abstract mesh');
        return;
    }

    const cubeBoundingBox = cubeMesh.getBoundingInfo().boundingBox;
    const cubeMin = cubeBoundingBox.minimumWorld;
    const cubeMax = cubeBoundingBox.maximumWorld;

    const updatedVertexData = removeVertices(
        vertexData,
        cubeMin,
        cubeMax,
        parentMatrix
    );
    if (!updatedVertexData) {
        console.error('Failed to update vertex data');
        return;
    }

    // Apply the updated vertex data back to the mesh
    updatedVertexData.applyToMesh(mesh, true);
    // cubeMesh.dispose();
}

function removeVertices(
    vertexData: VertexData,
    cubeMin: Vector3,
    cubeMax: Vector3,
    parentMatrix: Matrix
): VertexData | undefined {
    if (!vertexData.positions || !vertexData.indices) {
        console.error('Mesh vertex data is missing positions or indices');
        return;
    }

    const positions = Array.from(vertexData.positions);
    const indices = Array.from(vertexData.indices);
    const uvs = vertexData.uvs ? Array.from(vertexData.uvs) : [];
    const normals = vertexData.normals ? Array.from(vertexData.normals) : [];

    const newPositions: number[] = [];
    const newIndices: number[] = [];
    const newUVs: number[] = [];
    const newNormals: number[] = [];

    function transformVertex(vertex: Vector3): Vector3 {
        return Vector3.TransformCoordinates(vertex, parentMatrix);
    }

    function getVertex(index: number): Vector3 {
        return transformVertex(
            new Vector3(
                positions[index * 3],
                positions[index * 3 + 1],
                positions[index * 3 + 2]
            )
        );
    }

    function getUV(index: number): Vector2 {
        return new Vector2(uvs[index * 2], uvs[index * 2 + 1]);
    }

    function getNormal(index: number): Vector3 {
        return new Vector3(
            normals[index * 3],
            normals[index * 3 + 1],
            normals[index * 3 + 2]
        );
    }

    function addVertex(
        vertex: Vector3,
        uv?: Vector2,
        normal?: Vector3
    ): number {
        const index = newPositions.length / 3;
        newPositions.push(vertex.x, vertex.y, vertex.z);
        if (uv) {
            newUVs.push(uv.x, uv.y);
        }
        if (normal) {
            newNormals.push(normal.x, normal.y, normal.z);
        }
        return index;
    }

    function getIntersectionPoint(
        p1: Vector3,
        p2: Vector3,
        plane: Plane
    ): Vector3 | null {
        const lineDir = p2.subtract(p1);
        const denominator = Vector3.Dot(plane.normal, lineDir);

        if (Math.abs(denominator) < 1e-6) {
            return null; // The line is parallel to the plane
        }

        const t = -(Vector3.Dot(plane.normal, p1) + plane.d) / denominator;

        if (t < 0 || t > 1) {
            return null; // The intersection point is not within the segment
        }

        return p1.add(lineDir.scale(t));
    }

    const planes = [
        new Plane(1, 0, 0, -cubeMin.x), // Left
        new Plane(-1, 0, 0, cubeMax.x), // Right
        new Plane(0, 1, 0, -cubeMin.y), // Bottom
        new Plane(0, -1, 0, cubeMax.y), // Top
        new Plane(0, 0, 1, -cubeMin.z), // Front
        new Plane(0, 0, -1, cubeMax.z), // Back
    ];

    function isInsideCube(
        vertex: Vector3,
        min: Vector3,
        max: Vector3
    ): boolean {
        return (
            vertex.x >= min.x &&
            vertex.x <= max.x &&
            vertex.y >= min.y &&
            vertex.y <= max.y &&
            vertex.z >= min.z &&
            vertex.z <= max.z
        );
    }
    function getSignedDistanceToPlane(point: Vector3, plane: Plane): number {
        return Vector3.Dot(plane.normal, point) + plane.d;
    }
    function clipPolygonWithPlane(
        polygon: [Vector3, Vector2, Vector3][],
        plane: Plane
    ): [Vector3, Vector2, Vector3][] {
        const result: [Vector3, Vector2, Vector3][] = [];
        for (let i = 0; i < polygon.length; i++) {
            const currentVertex = polygon[i];
            const nextVertex = polygon[(i + 1) % polygon.length];

            const currentInside =
                getSignedDistanceToPlane(currentVertex[0], plane) >= 0;
            const nextInside =
                getSignedDistanceToPlane(nextVertex[0], plane) >= 0;

            if (currentInside) {
                result.push(currentVertex);
            }

            if (currentInside !== nextInside) {
                const intersectionPoint = getIntersectionPoint(
                    currentVertex[0],
                    nextVertex[0],
                    plane
                );
                if (intersectionPoint) {
                    const t =
                        Vector3.Distance(currentVertex[0], intersectionPoint) /
                        Vector3.Distance(currentVertex[0], nextVertex[0]);
                    const intersectionUV = Vector2.Lerp(
                        currentVertex[1],
                        nextVertex[1],
                        t
                    );
                    const intersectionNormal = Vector3.Lerp(
                        currentVertex[2],
                        nextVertex[2],
                        t
                    ).normalize();
                    result.push([
                        intersectionPoint,
                        intersectionUV,
                        intersectionNormal,
                    ]);
                }
            }
        }
        return result;
    }

    for (let i = 0; i < indices.length; i += 3) {
        const [i0, i1, i2] = [indices[i], indices[i + 1], indices[i + 2]];
        const [v0, v1, v2] = [getVertex(i0), getVertex(i1), getVertex(i2)];
        const [uv0, uv1, uv2] = [getUV(i0), getUV(i1), getUV(i2)];
        const [n0, n1, n2] = [getNormal(i0), getNormal(i1), getNormal(i2)];

        let polygon: [Vector3, Vector2, Vector3][] = [
            [v0, uv0, n0],
            [v1, uv1, n1],
            [v2, uv2, n2],
        ];

        planes.forEach((plane) => {
            polygon = clipPolygonWithPlane(polygon, plane);
        });

        if (polygon.length < 3) {
            continue; // Ignore degenerate polygons
        }

        const baseIndex = newPositions.length / 3;
        polygon.forEach(([position, uv, normal]) => {
            newPositions.push(position.x, position.y, position.z);
            newUVs.push(uv.x, uv.y);
            newNormals.push(normal.x, normal.y, normal.z);
        });

        for (let j = 1; j < polygon.length - 1; j++) {
            newIndices.push(baseIndex, baseIndex + j, baseIndex + j + 1);
        }
    }

    const updatedVertexData = new VertexData();
    updatedVertexData.positions = new Float32Array(newPositions);
    updatedVertexData.indices = new Uint32Array(newIndices);
    if (newUVs.length > 0) {
        updatedVertexData.uvs = new Float32Array(newUVs);
    }
    if (newNormals.length > 0) {
        updatedVertexData.normals = new Float32Array(newNormals);
    } else {
        updatedVertexData.normals = new Float32Array(newPositions.length);
        VertexData.ComputeNormals(
            updatedVertexData.positions,
            updatedVertexData.indices,
            updatedVertexData.normals
        );
    }

    return updatedVertexData;
}

Would you create a PG please?