Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect subtraction leading to additional fragments/paths #130

Open
TemperMichael opened this issue Oct 18, 2024 · 4 comments
Open

Incorrect subtraction leading to additional fragments/paths #130

TemperMichael opened this issue Oct 18, 2024 · 4 comments

Comments

@TemperMichael
Copy link

Hello, I am working on a game where you can cut holes into cubes where I add an additional mesh as a border as some kind of indicator.

If two holes overlap, I want to subtract the overlapping borders from each other, to create one big looking hole. This works in like 90% fine as you can see here:

IMG_0046

The problem is, sometimes there appear some strange fragments - they are only visible from one side, on the back they are invisible:

IMG_0045

Here is how I subtract them both from each other - I use a cube for the subtraction as I cutout three dimensional cubes, so when one hole is on the edge of the box, the border should be subtracted in three dimensions as well:

lastBorderMesh = lastBorderMesh
    .scaled(by: lastBorderScaleTransform)
    .rotated(by: lastBorderRotationTransform)
    .scaled(by: lastBorderParentScaleTransform)
    .translated(by: lastBorderTranslationTransform)
    .subtracting(
        Mesh.cube(size: 0.2, faces: .front)
            .scaled(by: scaleTransform)
            .rotated(by: rotationTransform)
            .scaled(by: secondScaleTransform)
            .translated(by: translationTransform)
    )
    .withMaterial(MaterialWrapper(borderMaterial))
    .translated(by: -lastBorderTranslationTransform)
    .scaled(by: Vector(1 / lastBorderParentScaleTransform.x, 1 / lastBorderParentScaleTransform.y, 1 /
lastBorderParentScaleTransform.z))
    .rotated(by: -lastBorderRotationTransform)
    .scaled(by: Vector(1 / lastBorderScaleTransform.x, 1 / lastBorderScaleTransform.y, 1 /
lastBorderScaleTransform.z))

currentBorderMesh = currentBorderMesh
    .scaled(by: scaleTransform)
    .rotated(by: rotationTransform)
    .scaled(by: secondScaleTransform)
    .translated(by: translationTransform)
    .subtracting(
        Mesh.cube(size: 0.16)
            .scaled(by: lastBorderScaleTransform)
            .rotated(by: lastBorderRotationTransform)
            .scaled(by: lastBorderParentScaleTransform)
            .translated(by: lastBorderTranslationTransform)
    )
    .translated(by: -translationTransform)
    .scaled(by: Vector(1 / secondScaleTransform.x, 1 / secondScaleTransform.y, 1 / secondScaleTransform.z))
    .rotated(by: -rotationTransform)
    .scaled(by: Vector(1 / scaleTransform.x, 1 / scaleTransform.y, 1 / scaleTransform.z))

Here is how I create the borders: My attempt is to place a cube with the size of the hole at the position where I want to cut out the hole, scale the original box by 1.1 and use the intersection of it with the hole sized cube to cut the outer part of the border, then doing the same with a 0.9 scaled size box to cut out the inner part of the border and lastly subtracting a 0.8 hole size cube to cut out the actual hole of the border (maybe there exists a better solution to that?)

    func createBorderMesh(
        for box: Mesh,
        translation: Vector,
        scale: Vector,
        rotation: Rotation,
        boxScale: Vector,
        material: RealityKit.Material
    ) -> Mesh {
        let outerHull = Mesh.cube(center: box.bounds.center, size: box.bounds.size.scaled(by: 1.1))
            .translated(by: -translation)
            .scaled(by: Vector(1 / boxScale.x, 1 / boxScale.y, 1 / boxScale.z))
            .rotated(by: -rotation)
            .scaled(by: Vector(1 / scale.x, 1 / scale.y, 1 / scale.z))
        
        let innerHull = Mesh.cube(center: box.bounds.center, size: box.bounds.size.scaled(by: 0.98))
            .translated(by: -translation)
            .scaled(by: Vector(1 / boxScale.x, 1 / boxScale.y, 1 / boxScale.z))
            .rotated(by: -rotation)
            .scaled(by: Vector(1 / scale.x, 1 / scale.y, 1 / scale.z))
        let innerBox = Mesh.cube(size: 0.16)
        let outerBox = Mesh.cube(size: 0.2)
        
        let final = outerHull
            .intersection(outerBox)
            .subtracting(innerBox)
            .subtracting(innerHull)
            .withMaterial(Euclid.MaterialWrapper(material))
        
        return final
    }

I already tried various attempts to find a solution on my side, but none of them worked, so I just wanted to make sure, that the problem is for sure on my side or if there is eventually a problem in the package?

The problem only occurs on device (Apple Vision Pro, visionOS 2.0) - I can also provide a TestFlight if this helps for debugging the issue.

@nicklockwood
Copy link
Owner

@TemperMichael I've seen issue like this before, and it's probably a floating point precision issue, but without the exact parameters it will be hard for me to reproduce or fix it. Can you provide a working code sample or project that demos it outside the context of your game?

It's interesting that it is only replicable on Vision Pro - that would tend to suggest that it might be due to some compiler setting (e.g. fast math), but again it's hard for me to verify.

Something that might help is calling makeWatertight() after performing CSG operations. This comes with a certain cost which is why I don't do it automatically, but it often helps to prevent the buildup of rounding errors that can lead to these kind of glitches. As a test you could also try adding isWatertight checks in your code to see if that might be the issue, and add makeWatertight() only in places where your meshes are failing that check.

@TemperMichael
Copy link
Author

I extracted the code from my app as an example project. So it shows a cube and wherever you tap it, a hole + border is placed and the latest border gets merged with the previous one if they overlap.

I already had some interesting findings when I extracted the code:

  1. As you expected, it seems like a floating point issue as it only happened as soon as I applied a combination of scaling and rotation values for the root object - without them, the fragments never appeared.
  2. The issue only seems to happen on the left, right and bottom side of the cube.
  3. I saw that I slightly adapted Euclid by commenting some of the asserts as a quick fix for prototyping my game - those now trigger again as I used the original git package for the example - maybe this causes the issue? (Plane.swift -> Line 197 assert(normal.isNormalized) and Mesh.swift -> Line 493 assert(!isConvex || polygons.groupedBySubmesh().count <= 1)) If you want to see the glitches without the app crashing, you basically just have to comment those two lines
  4. The issue now also appears on simulator

I tried to comment everything a bit for better understanding. There are also two files for placing the hole - you can just comment the other one to try one of the versions:

  • "PlaceHole_Glitchier_Version.swift" is the version that I posted here where the glitch appears on the edges AND the sides of the cube
  • "PlaceHole_LessGlitchier_Version.swift" is already a bit of an improved version where I subtract the borders directly on the creation of them from each other where the glitches only happen on the edges of the box

BorderBugExample.zip

Bonus question: I also adapted the package by making the MaterialWrapper public as I couldn't find a way to use RealityKit Materials otherwise. Could you tell me how I could use them without that adaption?

Sorry if some of those issues are maybe some general knowledge when it comes to 3D modifications, it's just my very first 3D game so it's more of a trial and error workflow for me. Therefore I also already tried you makeWaterTight() without really knowing what it does, but unfortunately without any solution to the problem.

@nicklockwood
Copy link
Owner

OK, I'm able to reproduce the issue. I'll see what I can do to fix it

@nicklockwood
Copy link
Owner

Bonus question: I also adapted the package by making the MaterialWrapper public as I couldn't find a way to use RealityKit Materials otherwise. Could you tell me how I could use them without that adaption?

Unfortunately the way that Apple has implemented RealityKit Materials makes them quite difficult to work with outside of the RealityKit framework itself. The recommended approach for now would be to use some other identifier for your materials and then replace them when you convert the Mesh to a RealityKit model at the last step.

A simple approach might be to store your RealityKit materials in an Array or Dictionary and use the keys/indices as the material values in Euclid. Then when you create your ModelEntity (or whatever), there's a materialLookup closure you can pass to get the actual RealityKit Material.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants