@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 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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2014-2021 Quenty
3
+ Copyright (c) 2014-2022 Quenty
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/geometryutils",
3
- "version": "2.2.0",
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
- "gitHead": "501844bd6c3d3f765fd3032b997d8030bc963a1f"
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
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }