/
VRTK_BezierPointerRenderer.cs
369 lines (324 loc) · 17.2 KB
/
VRTK_BezierPointerRenderer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
// Bezier Pointer Renderer|PointerRenderers|10030
namespace VRTK
{
using UnityEngine;
/// <summary>
/// The Bezier Pointer Renderer emits a curved line (made out of game objects) from the end of the attached object to a point on a ground surface (at any height).
/// </summary>
/// <remarks>
/// It is more useful than the Simple Pointer Renderer for traversing objects of various heights as the end point can be curved on top of objects that are not visible to the user.
///
/// > The bezier curve generation code is in another script located at `VRTK/Scripts/Internal/VRTK_CurveGenerator.cs` and was heavily inspired by the tutorial and code from [Catlike Coding](http://catlikecoding.com/unity/tutorials/curves-and-splines/).
/// </remarks>
/// <example>
/// `VRTK/Examples/009_Controller_BezierPointer` is used in conjunction with the Height Adjust Teleporter shows how it is possible to traverse different height objects using the curved pointer without needing to see the top of the object.
///
/// `VRTK/Examples/036_Controller_CustomCompoundPointer' shows how to display an object (a teleport beam) only if the teleport location is valid, and can create an animated trail along the tracer curve.
/// </example>
public class VRTK_BezierPointerRenderer : VRTK_BasePointerRenderer
{
[Header("Bezier Pointer Appearance Settings")]
[Tooltip("The maximum length of the projected forward beam.")]
public float maximumLength = 10f;
[Tooltip("The number of items to render in the bezier curve tracer beam. A high number here will most likely have a negative impact of game performance due to large number of rendered objects.")]
public int tracerDensity = 10;
[Tooltip("The size of the ground cursor. This number also affects the size of the objects in the bezier curve tracer beam. The larger the radius, the larger the objects will be.")]
public float cursorRadius = 0.5f;
[Header("Bezier Pointer Render Settings")]
[Tooltip("The maximum angle in degrees of the origin before the beam curve height is restricted. A lower angle setting will prevent the beam being projected high into the sky and curving back down.")]
[Range(1, 100)]
public float heightLimitAngle = 100f;
[Tooltip("The amount of height offset to apply to the projected beam to generate a smoother curve even when the beam is pointing straight.")]
public float curveOffset = 1f;
[Tooltip("Rescale each tracer element according to the length of the Bezier curve.")]
public bool rescaleTracer = false;
[Tooltip("The cursor will be rotated to match the angle of the target surface if this is true, if it is false then the pointer cursor will always be horizontal.")]
public bool cursorMatchTargetRotation = false;
[Tooltip("The number of points along the bezier curve to check for an early beam collision. Useful if the bezier curve is appearing to clip through teleport locations. 0 won't make any checks and it will be capped at `Pointer Density`. The higher the number, the more CPU intensive the checks become.")]
public int collisionCheckFrequency = 0;
[Header("Bezier Pointer Custom Appearance Settings")]
[Tooltip("A custom game object to use as the appearance for the pointer tracer. If this is empty then a collection of Sphere primitives will be created and used.")]
public GameObject customTracer;
[Tooltip("A custom game object to use as the appearance for the pointer cursor. If this is empty then a Cylinder primitive will be created and used.")]
public GameObject customCursor;
[Tooltip("A custom game object can be applied here to appear only if the location is valid.")]
public GameObject validLocationObject = null;
[Tooltip("A custom game object can be applied here to appear only if the location is invalid.")]
public GameObject invalidLocationObject = null;
protected VRTK_CurveGenerator actualTracer;
protected GameObject actualContainer;
protected GameObject actualCursor;
protected GameObject actualValidLocationObject = null;
protected GameObject actualInvalidLocationObject = null;
protected Vector3 fixedForwardBeamForward;
protected bool tracerVisible;
protected bool cursorVisible;
/// <summary>
/// The UpdateRenderer method is used to run an Update routine on the pointer.
/// </summary>
public override void UpdateRenderer()
{
if ((controllingPointer && controllingPointer.IsPointerActive()) || tracerVisible || cursorVisible)
{
Vector3 jointPosition = ProjectForwardBeam();
Vector3 downPosition = ProjectDownBeam(jointPosition);
AdjustForEarlyCollisions(jointPosition, downPosition);
MakeRenderersVisible();
}
base.UpdateRenderer();
}
protected override void ToggleRenderer(bool pointerState, bool actualState)
{
TogglePointerCursor(pointerState, actualState);
TogglePointerTracer(pointerState, actualState);
}
protected override void CreatePointerObjects()
{
actualContainer = new GameObject(string.Format("[{0}]BezierPointerRenderer_Container", gameObject.name));
VRTK_PlayerObject.SetPlayerObject(actualContainer, VRTK_PlayerObject.ObjectTypes.Pointer);
actualContainer.SetActive(false);
CreateTracer();
CreateCursor();
Toggle(false, false);
if (controllingPointer)
{
controllingPointer.ResetActivationTimer(true);
}
}
protected override void DestroyPointerObjects()
{
if (actualCursor != null)
{
Destroy(actualCursor);
}
if (actualTracer != null)
{
Destroy(actualTracer);
}
if (actualContainer != null)
{
Destroy(actualContainer);
}
}
protected override void UpdateObjectInteractor()
{
base.UpdateObjectInteractor();
//if the object interactor is too far from the pointer tip then set it to the pointer tip position to prevent glitching.
if (objectInteractor && actualCursor && Vector3.Distance(objectInteractor.transform.position, actualCursor.transform.position) > 0f)
{
objectInteractor.transform.position = actualCursor.transform.position;
}
}
protected override void ChangeMaterial(Color givenColor)
{
base.ChangeMaterial(givenColor);
ChangeMaterialColor(actualCursor, givenColor);
}
protected virtual void CreateTracer()
{
actualTracer = actualContainer.gameObject.AddComponent<VRTK_CurveGenerator>();
actualTracer.transform.SetParent(null);
actualTracer.Create(tracerDensity, cursorRadius, customTracer, rescaleTracer);
}
protected virtual GameObject CreateCursorObject()
{
float cursorYOffset = 0.02f;
GameObject createdCursor = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
MeshRenderer createdCursorRenderer = createdCursor.GetComponent<MeshRenderer>();
createdCursor.transform.localScale = new Vector3(cursorRadius, cursorYOffset, cursorRadius);
createdCursorRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
createdCursorRenderer.receiveShadows = false;
createdCursorRenderer.material = defaultMaterial;
Destroy(createdCursor.GetComponent<CapsuleCollider>());
return createdCursor;
}
protected virtual void CreateCursorLocations()
{
if (validLocationObject != null)
{
actualValidLocationObject = Instantiate(validLocationObject);
actualValidLocationObject.name = string.Format("[{0}]BezierPointerRenderer_ValidLocation", gameObject.name);
actualValidLocationObject.transform.SetParent(actualCursor.transform);
actualValidLocationObject.layer = LayerMask.NameToLayer("Ignore Raycast");
actualValidLocationObject.SetActive(false);
}
if (invalidLocationObject != null)
{
actualInvalidLocationObject = Instantiate(invalidLocationObject);
actualInvalidLocationObject.name = string.Format("[{0}]BezierPointerRenderer_InvalidLocation", gameObject.name);
actualInvalidLocationObject.transform.SetParent(actualCursor.transform);
actualInvalidLocationObject.layer = LayerMask.NameToLayer("Ignore Raycast");
actualInvalidLocationObject.SetActive(false);
}
}
protected virtual void CreateCursor()
{
actualCursor = (customCursor ? Instantiate(customCursor) : CreateCursorObject());
CreateCursorLocations();
actualCursor.name = string.Format("[{0}]BezierPointerRenderer_Cursor", gameObject.name);
VRTK_PlayerObject.SetPlayerObject(actualCursor, VRTK_PlayerObject.ObjectTypes.Pointer);
actualCursor.layer = LayerMask.NameToLayer("Ignore Raycast");
actualCursor.SetActive(false);
}
protected virtual Vector3 ProjectForwardBeam()
{
Transform origin = GetOrigin();
float attachedRotation = Vector3.Dot(Vector3.up, origin.forward.normalized);
float calculatedLength = maximumLength;
Vector3 useForward = origin.forward;
if ((attachedRotation * 100f) > heightLimitAngle)
{
useForward = new Vector3(useForward.x, fixedForwardBeamForward.y, useForward.z);
var controllerRotationOffset = 1f - (attachedRotation - (heightLimitAngle / 100f));
calculatedLength = (maximumLength * controllerRotationOffset) * controllerRotationOffset;
}
else
{
fixedForwardBeamForward = origin.forward;
}
var actualLength = calculatedLength;
Ray pointerRaycast = new Ray(origin.position, useForward);
RaycastHit collidedWith;
var hasRayHit = Physics.Raycast(pointerRaycast, out collidedWith, calculatedLength, ~layersToIgnore);
float contactDistance = 0f;
//reset if beam not hitting or hitting new target
if (!hasRayHit || (destinationHit.collider && destinationHit.collider != collidedWith.collider))
{
contactDistance = 0f;
}
//check if beam has hit a new target
if (hasRayHit)
{
contactDistance = collidedWith.distance;
}
//adjust beam length if something is blocking it
if (hasRayHit && contactDistance < calculatedLength)
{
actualLength = contactDistance;
}
//Use BEAM_ADJUST_OFFSET to move point back and up a bit to prevent beam clipping at collision point
return (pointerRaycast.GetPoint(actualLength - BEAM_ADJUST_OFFSET) + (Vector3.up * BEAM_ADJUST_OFFSET));
}
protected virtual Vector3 ProjectDownBeam(Vector3 jointPosition)
{
Vector3 downPosition = Vector3.zero;
Ray projectedBeamDownRaycast = new Ray(jointPosition, Vector3.down);
RaycastHit collidedWith;
var downRayHit = Physics.Raycast(projectedBeamDownRaycast, out collidedWith, float.PositiveInfinity, ~layersToIgnore);
if (!downRayHit || (destinationHit.collider && destinationHit.collider != collidedWith.collider))
{
if (destinationHit.collider != null)
{
PointerExit(destinationHit);
}
destinationHit = new RaycastHit();
downPosition = projectedBeamDownRaycast.GetPoint(0f);
}
if (downRayHit)
{
downPosition = projectedBeamDownRaycast.GetPoint(collidedWith.distance);
PointerEnter(collidedWith);
destinationHit = collidedWith;
}
return downPosition;
}
protected virtual void AdjustForEarlyCollisions(Vector3 jointPosition, Vector3 downPosition)
{
Vector3 newDownPosition = downPosition;
Vector3 newJointPosition = jointPosition;
if (collisionCheckFrequency > 0)
{
collisionCheckFrequency = Mathf.Clamp(collisionCheckFrequency, 0, tracerDensity);
Vector3[] beamPoints = new Vector3[]
{
GetOrigin().position,
jointPosition + new Vector3(0f, curveOffset, 0f),
downPosition,
downPosition,
};
Vector3[] checkPoints = actualTracer.GetPoints(beamPoints);
int checkFrequency = tracerDensity / collisionCheckFrequency;
for (int i = 0; i < tracerDensity - checkFrequency; i += checkFrequency)
{
var currentPoint = checkPoints[i];
var nextPoint = (i + checkFrequency < checkPoints.Length ? checkPoints[i + checkFrequency] : checkPoints[checkPoints.Length - 1]);
var nextPointDirection = (nextPoint - currentPoint).normalized;
var nextPointDistance = Vector3.Distance(currentPoint, nextPoint);
Ray checkCollisionRay = new Ray(currentPoint, nextPointDirection);
RaycastHit checkCollisionHit;
if (Physics.Raycast(checkCollisionRay, out checkCollisionHit, nextPointDistance, ~layersToIgnore))
{
var collisionPoint = checkCollisionRay.GetPoint(checkCollisionHit.distance);
Ray downwardCheckRay = new Ray(collisionPoint + (Vector3.up * 0.01f), Vector3.down);
RaycastHit downwardCheckHit;
if (Physics.Raycast(downwardCheckRay, out downwardCheckHit, float.PositiveInfinity, ~layersToIgnore))
{
destinationHit = downwardCheckHit;
newDownPosition = downwardCheckRay.GetPoint(downwardCheckHit.distance); ;
newJointPosition = (newDownPosition.y < jointPosition.y ? new Vector3(newDownPosition.x, jointPosition.y, newDownPosition.z) : jointPosition);
break;
}
}
}
}
DisplayCurvedBeam(newJointPosition, newDownPosition);
SetPointerCursor();
}
protected virtual void DisplayCurvedBeam(Vector3 jointPosition, Vector3 downPosition)
{
Vector3[] beamPoints = new Vector3[]
{
GetOrigin(false).position,
jointPosition + new Vector3(0f, curveOffset, 0f),
downPosition,
downPosition,
};
var tracerMaterial = (customTracer ? null : defaultMaterial);
actualTracer.SetPoints(beamPoints, tracerMaterial, currentColor);
if (tracerVisibility == VisibilityStates.AlwaysOff)
{
TogglePointerTracer(false, false);
}
else if(controllingPointer)
{
TogglePointerTracer(controllingPointer.IsPointerActive(), controllingPointer.IsPointerActive());
}
}
protected virtual void TogglePointerCursor(bool pointerState, bool actualState)
{
ToggleElement(actualCursor, pointerState, actualState, cursorVisibility, ref cursorVisible);
}
protected virtual void TogglePointerTracer(bool pointerState, bool actualState)
{
tracerVisible = (tracerVisibility == VisibilityStates.AlwaysOn ? true : pointerState);
actualTracer.TogglePoints(tracerVisible);
}
protected virtual void SetPointerCursor()
{
if (controllingPointer && destinationHit.transform)
{
TogglePointerCursor(controllingPointer.IsPointerActive(), controllingPointer.IsPointerActive());
actualCursor.transform.position = destinationHit.point;
if (cursorMatchTargetRotation)
{
actualCursor.transform.rotation = Quaternion.FromToRotation(Vector3.up, destinationHit.normal);
}
base.UpdateDependencies(actualCursor.transform.position);
ChangeColor(validCollisionColor);
if (actualValidLocationObject)
{
actualValidLocationObject.SetActive(ValidDestination());
}
if (actualInvalidLocationObject)
{
actualInvalidLocationObject.SetActive(!ValidDestination());
}
}
else
{
TogglePointerCursor(false, false);
ChangeColor(invalidCollisionColor);
}
}
}
}