@quenty/geometryutils 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE.md +1 -1
- package/package.json +6 -2
- package/src/Shared/CameraPyramidUtils.lua +194 -0
- package/src/node_modules.project.json +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [2.3.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/geometryutils@2.2.0...@quenty/geometryutils@2.3.0) (2022-12-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add CameraPyramidUtils.rayIntersection(camera, rayOrigin, unitRayDirection, debugMaid) ([e728752](https://github.com/Quenty/NevermoreEngine/commit/e7287526b8eb78e6b702a73b56d6304e02c355de))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [2.2.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/geometryutils@2.1.1...@quenty/geometryutils@2.2.0) (2022-03-27)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @quenty/geometryutils
|
package/LICENSE.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/geometryutils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Utility functions involving 3D and 2D geometry",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -27,5 +27,9 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@quenty/draw": "^4.2.0",
|
|
32
|
+
"@quenty/loader": "^6.0.1"
|
|
33
|
+
},
|
|
34
|
+
"gitHead": "d9b0d10faa443cc42a6c2ac966f2f56d124bbde5"
|
|
31
35
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class CameraPyramidUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local PlaneUtils = require("PlaneUtils")
|
|
8
|
+
local Draw = require("Draw")
|
|
9
|
+
|
|
10
|
+
local CameraPyramidUtils = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
--[=[
|
|
15
|
+
Treating the camera like a pyramid, compute points on the screen that the ray intersects with the
|
|
16
|
+
screen.
|
|
17
|
+
|
|
18
|
+
Returns the screen points in the same order as the ray orientation, such that the line is always
|
|
19
|
+
moving away from the ray.
|
|
20
|
+
|
|
21
|
+
@param camera Camera
|
|
22
|
+
@param rayOrigin Vector3
|
|
23
|
+
@param unitRayDirection Vector3
|
|
24
|
+
@param debugMaid Maid? -- Optional debug maid
|
|
25
|
+
@return Vector3? -- Screen point1
|
|
26
|
+
@return Vector3? -- Screen point2
|
|
27
|
+
]=]
|
|
28
|
+
function CameraPyramidUtils.rayIntersection(camera, rayOrigin, unitRayDirection, debugMaid)
|
|
29
|
+
assert(typeof(rayOrigin) == "Vector3", "Bad rayOrigin")
|
|
30
|
+
assert(typeof(unitRayDirection) == "Vector3", "Bad unitRayDirection")
|
|
31
|
+
|
|
32
|
+
unitRayDirection = unitRayDirection.unit
|
|
33
|
+
local camCFrame = camera.CFrame
|
|
34
|
+
local viewportSize = camera.ViewportSize
|
|
35
|
+
if viewportSize.x == 0 or viewportSize.y == 0 then
|
|
36
|
+
return nil, nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local aspectRatio = viewportSize.x/viewportSize.y
|
|
40
|
+
local halfVerticalFov = math.rad(camera.FieldOfView/2)
|
|
41
|
+
local halfHorizontalFov = math.atan(math.tan(halfVerticalFov)*aspectRatio)
|
|
42
|
+
|
|
43
|
+
-- Construct pyramid with normals facing out
|
|
44
|
+
local origin = camCFrame.p
|
|
45
|
+
local cframeTop = (camCFrame * CFrame.Angles(halfVerticalFov, 0, 0))
|
|
46
|
+
local cframeBottom = (camCFrame * CFrame.Angles(-halfVerticalFov, 0, 0))
|
|
47
|
+
local cframeLeft = (camCFrame * CFrame.Angles(0, halfHorizontalFov, 0))
|
|
48
|
+
local cframeRight = (camCFrame * CFrame.Angles(0, -halfHorizontalFov, 0))
|
|
49
|
+
|
|
50
|
+
local normalTop = cframeTop.YVector
|
|
51
|
+
local normalBottom = -cframeBottom.YVector
|
|
52
|
+
local normalLeft = -cframeLeft.XVector -- these are flipped because the camera CFrame is flipped
|
|
53
|
+
local normalRight = cframeRight.XVector
|
|
54
|
+
|
|
55
|
+
local intersectionTop, distTop = PlaneUtils.rayIntersection(origin, normalTop, rayOrigin, unitRayDirection)
|
|
56
|
+
local intersectionBottom, distBottom = PlaneUtils.rayIntersection(origin, normalBottom, rayOrigin, unitRayDirection)
|
|
57
|
+
local intersectionLeft, distLeft = PlaneUtils.rayIntersection(origin, normalLeft, rayOrigin, unitRayDirection)
|
|
58
|
+
local intersectionRight, distRight = PlaneUtils.rayIntersection(origin, normalRight, rayOrigin, unitRayDirection)
|
|
59
|
+
|
|
60
|
+
local topInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionTop, halfHorizontalFov, true)
|
|
61
|
+
local bottomInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionBottom, halfHorizontalFov, true)
|
|
62
|
+
local leftInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionLeft, halfVerticalFov, false)
|
|
63
|
+
local rightInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionRight, halfVerticalFov, false)
|
|
64
|
+
|
|
65
|
+
local inBoundsIntersections = {}
|
|
66
|
+
if topInBounds then
|
|
67
|
+
table.insert(inBoundsIntersections, {
|
|
68
|
+
point = intersectionTop;
|
|
69
|
+
dist = distTop;
|
|
70
|
+
})
|
|
71
|
+
end
|
|
72
|
+
if bottomInBounds then
|
|
73
|
+
table.insert(inBoundsIntersections, {
|
|
74
|
+
point = intersectionBottom;
|
|
75
|
+
dist = distBottom;
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
if leftInBounds then
|
|
79
|
+
table.insert(inBoundsIntersections, {
|
|
80
|
+
point = intersectionLeft;
|
|
81
|
+
dist = distLeft;
|
|
82
|
+
})
|
|
83
|
+
end
|
|
84
|
+
if rightInBounds then
|
|
85
|
+
table.insert(inBoundsIntersections, {
|
|
86
|
+
point = intersectionRight;
|
|
87
|
+
dist = distRight;
|
|
88
|
+
})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if debugMaid then
|
|
92
|
+
debugMaid._top = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionTop, topInBounds)
|
|
93
|
+
debugMaid._bottom = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionBottom, bottomInBounds)
|
|
94
|
+
debugMaid._left = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionLeft, leftInBounds)
|
|
95
|
+
debugMaid._right = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionRight, rightInBounds)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if #inBoundsIntersections == 0 then
|
|
99
|
+
return nil, nil
|
|
100
|
+
elseif #inBoundsIntersections == 1 then
|
|
101
|
+
-- Happens when the other point fades off into the distance on this one screen such that it never ends
|
|
102
|
+
local data = inBoundsIntersections[1]
|
|
103
|
+
local intersection = data.point
|
|
104
|
+
local firstViewportPoint = camera:WorldToViewportPoint(intersection)
|
|
105
|
+
|
|
106
|
+
local firstOption, firstOptionOnScreen = camera:WorldToViewportPoint(intersection + unitRayDirection * 10000)
|
|
107
|
+
local secondOption, secondOptionOnScreen = camera:WorldToViewportPoint(intersection - unitRayDirection * 10000)
|
|
108
|
+
|
|
109
|
+
local secondViewportPoint
|
|
110
|
+
if firstOptionOnScreen then
|
|
111
|
+
secondViewportPoint = firstOption
|
|
112
|
+
elseif secondOptionOnScreen then
|
|
113
|
+
secondViewportPoint = secondOption
|
|
114
|
+
else
|
|
115
|
+
warn("Failed to find option on screen")
|
|
116
|
+
return nil, nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
-- Flip around
|
|
120
|
+
if data.dist < 0 then
|
|
121
|
+
firstViewportPoint, secondViewportPoint = secondViewportPoint, firstViewportPoint
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
return firstViewportPoint, secondViewportPoint
|
|
125
|
+
else
|
|
126
|
+
local first = inBoundsIntersections[1]
|
|
127
|
+
local second = inBoundsIntersections[2]
|
|
128
|
+
|
|
129
|
+
if first.dist > second.dist then
|
|
130
|
+
first, second = second, first
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
local firstScreenPoint = camera:WorldToViewportPoint(first.point)
|
|
134
|
+
local secondScreenPoint = camera:WorldToViewportPoint(second.point)
|
|
135
|
+
return firstScreenPoint, secondScreenPoint
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
function CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersection, inBounds)
|
|
140
|
+
if not inBounds then
|
|
141
|
+
return nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
local halfVerticalFov = math.rad(camera.FieldOfView/2)
|
|
145
|
+
local viewportSize = camera.ViewportSize
|
|
146
|
+
local PIXELS_DIAMETER = 40
|
|
147
|
+
local PIXELS_OFFSET = 5 + PIXELS_DIAMETER/2
|
|
148
|
+
|
|
149
|
+
local position = camera:WorldToViewportPoint(intersection)
|
|
150
|
+
local dist = position.z
|
|
151
|
+
local worldHeight = 2*math.tan(halfVerticalFov)*dist
|
|
152
|
+
local scale = worldHeight/viewportSize.y
|
|
153
|
+
|
|
154
|
+
local firstPoint = intersection + PIXELS_OFFSET*unitRayDirection*scale
|
|
155
|
+
local secondPoint = intersection - PIXELS_OFFSET*unitRayDirection*scale
|
|
156
|
+
|
|
157
|
+
local _, onScreen1 = camera:WorldToViewportPoint(firstPoint)
|
|
158
|
+
local _, onScreen2 = camera:WorldToViewportPoint(secondPoint)
|
|
159
|
+
|
|
160
|
+
local color = Color3.new(1, 1, 0)
|
|
161
|
+
if onScreen1 then
|
|
162
|
+
return Draw.point(firstPoint, color, nil, PIXELS_DIAMETER*scale)
|
|
163
|
+
elseif onScreen2 then
|
|
164
|
+
return Draw.point(secondPoint, color, nil, PIXELS_DIAMETER*scale)
|
|
165
|
+
else
|
|
166
|
+
return nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
function CameraPyramidUtils._isInBounds(camCFrame, intersection, halfFov, isVertical)
|
|
171
|
+
if not intersection then
|
|
172
|
+
return false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
local relative = camCFrame:pointToObjectSpace(intersection)
|
|
176
|
+
local dist = -relative.z
|
|
177
|
+
|
|
178
|
+
if dist < 0 then
|
|
179
|
+
return false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
-- Discard the other information (we're projecting onto the flat camera plane)
|
|
183
|
+
local horizontalDist
|
|
184
|
+
if isVertical then
|
|
185
|
+
horizontalDist = math.abs(relative.x)
|
|
186
|
+
else
|
|
187
|
+
horizontalDist = math.abs(relative.y)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
local angle = math.atan2(horizontalDist, dist)
|
|
191
|
+
return angle <= halfFov
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return CameraPyramidUtils
|