Setting a model's CFrame offsets the parts slightly every time it's set

Hello.

I ran into an issue with my door system where if you open and close a door, the object’s parts will get more imprecise and incorrectly placed every time the door is interacted with.

Before interacting

After interacting

The way my door system works is that it sets the PrimaryPart’s CFrame of the door model to point toward another part using a tween.
Very simple, yet it causes this unappealing bug.

Is this due to how many parts I have in my model?
The current count is 51. However, I plan on using a mesh instead.
If I were to use a mesh and one extra part, would this effect still occur?

Thanks in advance.

3 Likes

Floating point errors, which causes parts to be ever so slightly out of place.

I would recommend using less parts.

3 Likes

The effect can still occur with two mesh parts, but will only be noticeable if you still have parts like this which share edges. If you have something like a door mesh and a doorknob mesh, were the exact placement of the latter is visually indifferent to small numerical errors, you’ll be good.

This problem is caused by two types of floating point problems: finite precision and accumulated error. Even when you have a part that starts out axis-aligned and with nice integer coordinates, rotating it inevitably results in having to approximate the new vertex locations (the mathematical ideal coordinates can be irrational, so no amount of precision is enough). When you rotate things that have different centers, they can end up just a bit off… rounded in different directions, essentially. From tween to tween, the original position is lost. Tween #2 starts with wherever tween #1 left off, which could be misaligned, so each tween can potentially add new error. This is accumulated error. This isn’t unique to Roblox, or even 3D. If you do something like rotate an image in Photoshop by an arbitrary amount (not some nice multiple of 90 degrees), the same loss of precision occurs, and you can’t rotate the image back and recover it without some loss of quality. I mean by rotating again, not using “Undo” in the same session :slight_smile:

4 Likes

It’s been a known issue that using Model:SetPrimaryPartCFrame() causes floating point errors. It’s pretty difficult to solve something like this on Roblox’s end, unfortunately.

Basically the more you call the function, the more it offsets everything that’s not the primary part. A simple-but-heavy-memory solution would be to initially cache the offsets with their associated part and then apply them after you change the primary part’s CFrame. Here’s a short function (that may error because I’m not in a position to check lol) that may fix your problem as long as the door parts don’t need to shift around or anything.

local offsetCache = {}
local function SetPrimaryPartCFrame(model,cframe)

    assert(model and model.ClassName == "Model" and model.PrimaryPart,"Bad or non-model")
    assert(cframe ~= nil,"CFrame doesn't exist!")
    
    local function getOffsets(model)
        local offsets = {}
        local function search(c)
            if c:IsA"BasePart" and c ~= model.PrimaryPart then
                offsets[c] = model.PrimaryPart.CFrame:inverse() * c.CFrame
            end
            for i,v in next,c:GetChildren() do
                search(v)
            end
        end
        search(model)
        offsetCache[model] = offsets
    end

    local offsets = offsetCache[model] or getOffsets(model)
    model.PrimaryPart.CFrame = cframe
    for part,offset in next,offsets do
        part.CFrame = model.PrimaryPart.CFrame * offset
    end
end
4 Likes

You may also find this a useful reference, although note that it is essentially the same solution that Locard has offered above.

1 Like

It’s a good idea in theory, but unfortunately in the world of single-precision floats, even fundamental rules like AA⁻¹ = I don’t actually hold. Every CFrame multiplication can add floating point error except in very special cases. Easy to demonstrate, just run something like this:

local rng=Random.new() x=0 for i=1,1000000 do local a=CFrame.fromOrientation(rng:NextNumber(),rng:NextNumber(),rng:NextNumber()) if a*a:inverse()==CFrame.new() then x=x+1 end end print(x)

On my computer, it holds about 6700 times out of a million, or 0.67% of the time, and that’s a pure rotation matrix. Give that CFrame a translation component like this:

local rng=Random.new() x=0 for i=1,1000000 do local a=CFrame.fromOrientation(rng:NextNumber(),rng:NextNumber(),rng:NextNumber()) +Vector3.new(rng:NextNumber(),rng:NextNumber(),rng:NextNumber()) if a*a:inverse()==CFrame.new() then x=x+1 end end print(x)

And now it’s ~360 out of a million, or as good as never. This is because if the CFrame is not a pure rotation, its inverse is not just the transpose, so inverting it is yet another source of floating point error (pretty significant this time too).

If minimizing accumulated error is the goal, the best you can do is cache the original location and always move the part from its original location, to where you want it, with a single transform (a single CFrame multiply, no inversions). But even then, a rotation can still cause the issue seen in the OP, because of rounding errors and tiny differences in the coordinates of vertices that should be at the same location but actually aren’t to all digits of precision.

4 Likes

This seems to fix this issue. Would love to hear your thoughts on this!

1 Like