ClickDetectors Break When Camera CFrame Is Distorted Through Shearing

Introduction

What I am trying to do

I would like to get a customized camera working for an experience. In particular, I would like half of the screen to contain a UI and the center of the camera to be shifted onto the other half of the screen. The solution that works well is to distort the camera by shearing it along the X axis to shift the center to the left (with @Reselim’s distort.lua library). However, this distortion likely breaks the ClickDetector math.

If there were any other solutions to shift the camera focus onto half of the screen without distorting the camera, please let me know.

See the video below first demonstrating the scenarios in which ClickDetectors don’t break (camera rotation) and then demonstrating the shear which breaks the ClickDetectors. Observe the ClickDetector area very different than the part’s area displayed on camera after the shear.

Prior threads

  1. A thread where @boatbomber outlines a module that @Reselim made to change the camera CFrame to achieve this effect.
  2. A related ask with a good photo in it to illustrate the “half of the screen is UI” desire.
  3. Another ask from 6 years ago about this effect.

Reproducing the issue

Setup:

  1. Make a part of your choice.
  2. Make a ClickDetector the child of that part.
  3. Increase MaxActivationDistance so that it always is clickable.

You can also use the following baseplate, which includes some buttons to show the issue:
shear_click_detector.rbxl (49.7 KB)

Normal Behavior:

  1. Move around the part with the default camera scripts.
  2. Observe that the part is clickable only when you click inside it.
  3. Rotate the camera by 45 degrees using the commands:
workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
workspace.CurrentCamera.CFrame *= CFrame.Angles(0, math.rad(45), 0)
  1. Observe that the click detector still works properly in this scenario.

Bug:

  1. Distort the camera with a shear by using the following commands:
workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
workspace.CurrentCamera.CFrame *= CFrame.new(0,0,0,1,0,0,0,1,0,0.765598178,0,1)
  1. Observe that the ClickDetector doesn’t respect the camera shear. Empty space is clickable, and parts of the clickable part do not register the click detector.

Hypothesis

Shears introduce distortion to the camera. There is math that ClickDetectors use which does not account for this distortion.

Random background about CFrame math

@Kriko_YT wrote a pretty good overview of CFrame math here. I remember Roblox used to have a guide but I can’t find it anymore. However, the main thing is that they can be represented with the matrix:

r00 r01 r02 x
r10 r11 r12 y
r20 r21 r22 z
0 0 0 1

Certain transformations can be applied to that matrix.

The last relevant bit of background is around 3D cameras and how they can be represented with a frustum shape. This page has a decent overview.

Rotations

The first type of transformation we can apply is a rotation. In linear algebra, rotation matrices have two properties:

  1. They are orthogonal (they preserve length of vectors and angles between vectors)
  2. They have a determinant of 1 (they are not reflections)

Transformations around the Y axis can be computed by multiplying the CFrame matrices with the following matrix, where θ is the angle of rotation.

cosθ 0 sinθ 0
0 1 0 0
-sinθ 0 cosθ 0
0 0 0 1

We rotated 45 degrees before using CFrame.Angles(0, math.rad(45), 0), which is equivalent to us multiplying by:

0.707 0 0.707 0
0 1 0 0
-0.707 0 0.707 0
0 0 0 1

Due to the orthogonality of this rotation, we do not change the shape of the camera frustum at all here.

Shears

Unlike rotations, shears meaningfully change the shape of whatever they are transforming. Some pictures can be found here.

Earlier, we performed a shear along the x axis by changing the Z coordinates by the factor 0.76, equivalent to multiplying the camera CFrame by

1 0 0 0
0 1 0 0
0.765598178 0 1 0
0 0 0 1

We did this because we wanted to shift the camera position one quarter to the left. @Reselim’s distort.lua library calculates a -0.25 scale shift of the camera to the left with the formula:

-(-0.25) * math.tan(math.rad(camera.FieldOfView) / 2) * (camera.ViewportSize.X / camera.ViewportSize.Y) * 2

For my camera viewport size, that was 0.765598178.

This means that for every point on the camera, the higher X is (the further to the right a point is), the further that Z increases (as -Z is forward, +Z means that the point moves back to the camera.)

How can we visualize this affecting our camera? I’m not super clear on this stuff, but I guess we can imagine the right side of the near plane of the camera moving towards the position of the camera and the left side of the near plane moving away. While I’m not too confident about how to explain how the camera maintains its aspect ratio and changes the projection matrix, the effect of this is that stuff on the right becomes stretched out and “closer together” and stuff on the left becomes “farther apart” and less squashed together. Another way of potentially explaining this is that the effective field of view becomes smaller on the right than it does the left. If any computer graphics experts know about how to explain this better, would love to hear it!

In any case, my guess is that the ClickDetector math doesn’t take into account this distortion.

6 Likes

Thanks for the report! We’ve filed a ticket in our internal database.

2 Likes

Hi there, this method of distorting the camera is taking advantage of the quicks of our rendering engine and don’t work on any platforms other than Mac/Windows. You’ve brought up some good points on way we should support varying viewport sizes as an alternate feature that will work on all platforms, but we won’t actively fix bugs with non normalized cframes.