Unity SDK Docs 1.5.0-beta.6
Loading...
Searching...
No Matches
TableTopGizmo.cs
1/*
2 * Copyright (C) 2020-2023 Tilt Five, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17using UnityEngine;
18
19#if UNITY_EDITOR
20using UnityEditor;
21using System.Collections.Generic;
22
23namespace TiltFive
24{
28 public class TableTopGizmo {
29
30 #region Private Structs
31
32 private struct GameboardMesh
33 {
34 public string boardMeshAssetPath;
35 public string surfaceMeshChildPath;
36 public string borderMeshChildPath;
37 public string stageMeshChildPath;
38 public string stageCornerMeshChildPath;
39
40 public GameObject meshObj;
41 public Mesh meshSurface;
42 public Mesh meshBorder;
43 public Mesh meshStage;
44 public Mesh meshStageCorner;
45
46 public bool TryLoadMeshes()
47 {
48 bool allMeshesLoaded = meshObj != null && meshBorder != null && meshSurface != null
49 && meshStage != null && meshStageCorner != null;
50
51 if (!allMeshesLoaded)
52 {
53 LoadGameboardModel(boardMeshAssetPath, surfaceMeshChildPath, borderMeshChildPath,
54 stageMeshChildPath, stageCornerMeshChildPath,
55 out meshObj, out meshSurface, out meshBorder, out meshStage, out meshStageCorner);
56 }
57 return !allMeshesLoaded;
58 }
59
60 public void LoadGameboardModel(string meshAssetPath, string surfaceMeshChildPath, string borderMeshChildPath,
61 string stageMeshChildPath, string stageCornerMeshChildPath,
62 out GameObject meshObj, out Mesh meshSurface, out Mesh meshBorder, out Mesh meshStage, out Mesh meshStageCorner)
63 {
64 meshObj = AssetDatabase.LoadAssetAtPath<GameObject>(meshAssetPath);
65
66 TryGetMesh(surfaceMeshChildPath, out meshSurface);
67 TryGetMesh(borderMeshChildPath, out meshBorder);
68 TryGetMesh(stageMeshChildPath, out meshStage);
69 TryGetMesh(stageCornerMeshChildPath, out meshStageCorner);
70 }
71
72 private bool TryGetMesh(string meshPath, out Mesh resultMesh)
73 {
74 var meshTransform = meshObj.transform.Find(meshPath);
75 if(meshTransform != null && meshTransform.TryGetComponent<MeshFilter>(out var meshFilter))
76 {
77 resultMesh = meshFilter.sharedMesh;
78 return true;
79 }
80 resultMesh = null;
81 return false;
82 }
83 }
84
85 private struct LineSegment
86 {
87 public LineSegment(Vector3 start, Vector3 end, int LOD)
88 {
89 this.Start = start;
90 this.End = end;
91 this.LOD = LOD;
92 }
93 public Vector3 Start;
94 public Vector3 End;
95 public int LOD;
96 }
97
98 private struct GizmoReferenceFrame : System.IDisposable
99 {
100 private Matrix4x4 previousGizmoMatrix;
101
102 public GizmoReferenceFrame(Matrix4x4 temporaryGizmoReferenceFrame)
103 {
104 previousGizmoMatrix = Gizmos.matrix;
105 Gizmos.matrix = temporaryGizmoReferenceFrame;
106 }
107
108 public void Dispose()
109 {
110 Gizmos.matrix = previousGizmoMatrix;
111 }
112 }
113
114 private struct GizmoColor : System.IDisposable
115 {
116 private Color previousGizmoColor;
117 public GizmoColor(Color temporaryGizmoColor)
118 {
119 previousGizmoColor = Gizmos.color;
120 Gizmos.color = temporaryGizmoColor;
121 }
122
123 public GizmoColor(Color temporaryGizmoColor, float alphaOverride)
124 : this(new Color(temporaryGizmoColor.r, temporaryGizmoColor.g, temporaryGizmoColor.b, alphaOverride))
125 {
126 }
127
128 public GizmoColor(float r, float g, float b)
129 : this(new Color(r,g,b))
130 {
131 }
132
133 public GizmoColor(float r, float g, float b, float a)
134 : this(new Color(r, g, b, a))
135 {
136 }
137
138 public void Dispose()
139 {
140 Gizmos.color = previousGizmoColor;
141 }
142 }
143
144 #endregion Private Structs
145
146 private GameboardMesh gameboardMesh_LE = new GameboardMesh()
147 {
148 boardMeshAssetPath = MeshAssets.GetPathToGameboardMesh(GameboardType.GameboardType_LE),
149 surfaceMeshChildPath = "Surface_LE",
150 borderMeshChildPath = "Border_LE",
151 stageMeshChildPath = "Stage_LE",
152 stageCornerMeshChildPath = "Stage_Corner_LE"
153 };
154
155 private GameboardMesh gameboardMesh_XE = new GameboardMesh()
156 {
157 boardMeshAssetPath = MeshAssets.GetPathToGameboardMesh(GameboardType.GameboardType_XE),
158 surfaceMeshChildPath = "Surface_Flat",
159 borderMeshChildPath = "Border_Flat",
160 stageMeshChildPath = "Stage_Flat",
161 stageCornerMeshChildPath = "Stage_Corner_Flat"
162 };
163
164 private GameboardMesh gameboardMesh_XE_Raised = new GameboardMesh()
165 {
166 boardMeshAssetPath = MeshAssets.GetPathToGameboardMesh(GameboardType.GameboardType_XE_Raised),
167 surfaceMeshChildPath = "Surface_Raised",
168 borderMeshChildPath = "Border_Raised",
169 stageMeshChildPath = "Stage_Raised",
170 stageCornerMeshChildPath = "Stage_Corner_Raised"
171 };
172
173 private string logoMeshAssetPath = MeshAssets.GetPathToT5LogoMesh();
174 private string logoBorderChildPath = "Circle";
175 private string logoLeftCharacterChildPath = "Tilt";
176 private string logoRightCharacterChildPath = "Five";
177
178 private ScaleSettings scaleSettings;
179 private GameBoardSettings gameBoardSettings;
180 private float scaleToUWRLD_USTAGE => scaleSettings.GetScaleToWorldSpaceFromGameboardSpace(gameBoardSettings.gameBoardScale);
181 private GameBoard.GameboardExtents gameboardExtents = new GameBoard.GameboardExtents(
182 GameBoard.GameboardExtents.GAMEBOARD_SIZE_LE);
183
184 private float totalGameBoardWidthInMeters =>
185 usableGameBoardWidthInMeters + 2f * GameBoard.GameboardExtents.BORDER_WIDTH_IN_METERS;
186 private float totalGameBoardLengthInMeters =>
187 usableGameBoardLengthInMeters + 2f * GameBoard.GameboardExtents.BORDER_WIDTH_IN_METERS;
188
189 private float usableGameBoardWidthInMeters => gameboardExtents.ViewableSpanX.ToMeters;
190 private float usableGameBoardLengthInMeters => gameboardExtents.ViewableSpanZ.ToMeters;
191
192 private GameboardType gameboardType;
193
194 private GameObject logoObj;
195 private Mesh meshLogoBorder;
196 private Mesh meshLogoLeftCharacter;
197 private Mesh meshLogoRightCharacter;
198
199 private Mesh meshRuler;
200 private List<LineSegment> rulerData;
201
202
203 // The lines comprising the board gizmo's unit grid.
204 private List<LineSegment> xGridLines;
205 private List<LineSegment> zGridLines;
206 private float yGridOffset;
207
208 private readonly Color t5Orange = new Color(0.9450980392156862f, 0.34901960784313724f, 0.13333333333333333f);
209 private readonly Color t5LightGray = new Color(0.9607843137254902f, 0.9647058823529412f, 0.9647058823529412f);
210 private readonly Color t5Gray = new Color(0.3764705882352941f, 0.39215686274509803f, 0.4392156862745098f);
211 private void Configure(ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings,
212 float gridOffsetY, GameBoard.GameboardExtents gameboardExtents)
213 {
214 this.scaleSettings = scaleSettings;
215 this.gameBoardSettings = gameBoardSettings;
216 this.yGridOffset = gridOffsetY;
217 this.gameboardExtents = gameboardExtents;
218
219 gameboardMesh_LE.TryLoadMeshes();
220 gameboardMesh_XE.TryLoadMeshes();
221 gameboardMesh_XE_Raised.TryLoadMeshes();
222
223 if(null == logoObj)
224 {
225 logoObj = AssetDatabase.LoadAssetAtPath<GameObject>(logoMeshAssetPath);
226 }
227
228 if(null == meshLogoBorder)
229 {
230 var transform = logoObj.transform.Find( logoBorderChildPath );
231 if(transform != null)
232 {
233 if(transform.TryGetComponent<MeshFilter>(out var meshFilter))
234 {
235 meshLogoBorder = meshFilter.sharedMesh;
236 }
237 }
238 }
239 if(null == meshLogoLeftCharacter)
240 {
241 var transform = logoObj.transform.Find( logoLeftCharacterChildPath );
242 if(transform != null)
243 {
244 if(transform.TryGetComponent<MeshFilter>(out var meshFilter))
245 {
246 meshLogoLeftCharacter = meshFilter.sharedMesh;
247 }
248 }
249 }
250 if(null == meshLogoRightCharacter)
251 {
252 var transform = logoObj.transform.Find( logoRightCharacterChildPath );
253 if(transform != null)
254 {
255 if(transform.TryGetComponent<MeshFilter>(out var meshFilter))
256 {
257 meshLogoRightCharacter = meshFilter.sharedMesh;
258 }
259 }
260 }
261
262 var previousGameboardType = gameboardType;
263 gameboardType = gameBoardSettings.currentGameBoard.GetDisplayedGameboardType(gameBoardSettings);
264
265 bool gameboardTypeChanged = previousGameboardType != gameboardType;
266
267 if (null == xGridLines || null == zGridLines || gameboardTypeChanged)
268 {
269 ResetGrid();
270 }
271
272 if(null == meshRuler || gameboardTypeChanged)
273 {
274 ConstructRulerMesh(gameboardExtents.ViewableSpanX, "meshRuler", out meshRuler);
275 }
276 }
277
278 public void ConstructRulerMesh(Length rulerLength, string meshName, out Mesh meshRuler)
279 {
280 var rulerData = new List<LineSegment>();
281
282 float oneMillimeterLengthInMeters = new Length(1, LengthUnit.Millimeters).ToMeters;
283
284 // For the centimeter ruler, we're going to draw regular marks for centimeters, and smaller ones for millimeters.
285 for (int i = 0; i * oneMillimeterLengthInMeters < rulerLength.ToMeters; i++)
286 {
287 float currentDistance = i * (oneMillimeterLengthInMeters / rulerLength.ToMeters);
288 float smallestFractionOfBoardWidth = 1f / 150f;
289
290 float tickMarkLength = smallestFractionOfBoardWidth;
291 int lod = 3;
292
293 lod -= i % 5 == 0 ? 1 : 0;
294 lod -= i % 10 == 0 ? 1 : 0;
295
296 tickMarkLength += (3 - lod) * smallestFractionOfBoardWidth;
297
298 rulerData.Add(new LineSegment(new Vector3(currentDistance, 0f, 0f), new Vector3(currentDistance, 0f, tickMarkLength), lod));
299 }
300
301 float oneSixteenthInchLengthInMeters = new Length(1 / 16f, LengthUnit.Inches).ToMeters;
302
303 // For the inch ruler, we're going to draw regular marks for inches, and smaller ones for half/quarter/eighth/sixteenth inches.
304 for (int i = 0; i * oneSixteenthInchLengthInMeters < rulerLength.ToMeters; i++)
305 {
306 float currentDistance = i * (oneSixteenthInchLengthInMeters / rulerLength.ToMeters);
307 float smallestFractionOfBoardWidth = 1f / 300f;
308
309 float tickMarkLength = smallestFractionOfBoardWidth;
310 int lod = 5;
311
312 lod -= i % 2 == 0 ? 1 : 0;
313 lod -= i % 4 == 0 ? 1 : 0;
314 lod -= i % 8 == 0 ? 1 : 0;
315 lod -= i % 16 == 0 ? 1 : 0;
316
317 tickMarkLength += (5 - lod) * smallestFractionOfBoardWidth;
318
319 float offsetFromCentimeterRuler = 1 / 16f;
320 rulerData.Add(new LineSegment(
321 new Vector3(currentDistance, 0f, offsetFromCentimeterRuler - tickMarkLength),
322 new Vector3(currentDistance, 0f, offsetFromCentimeterRuler),
323 lod));
324 }
325
326 meshRuler = new Mesh();
327 meshRuler.name = meshName;
328
329 int vertArraySizeRatio = 4; // There are 4 vertices for every line in rulerData.
330 Vector3[] verts = new Vector3[rulerData.Count * vertArraySizeRatio];
331
332 int triArraySizeRatio = 6; // There are 6 triangle vertex indices for every line in rulerData.
333 int[] triangles = new int[rulerData.Count * triArraySizeRatio];
334
335 float lineThickness = 1 / 2800f;
336
337 // We want to offset the x vector component to achieve line thickness.
338 var lineThicknessOffset = Vector3.right * (lineThickness / 2f);
339
340 for (int i = 0; i < rulerData.Count; i++)
341 {
342 var line = rulerData[i];
343
344 var bottomLeft = line.Start - lineThicknessOffset + Vector3.left / 2 + Vector3.back / 32f;
345 var topLeft = line.End - lineThicknessOffset + Vector3.left / 2 + Vector3.back / 32f;
346
347 var bottomRight = line.Start + lineThicknessOffset + Vector3.left / 2 + Vector3.back / 32f;
348 var topRight = line.End + lineThicknessOffset + Vector3.left / 2 + Vector3.back / 32f;
349
350 var vertIndex = i * vertArraySizeRatio;
351 verts[vertIndex] = bottomLeft;
352 verts[vertIndex + 1] = topLeft;
353 verts[vertIndex + 2] = bottomRight;
354 verts[vertIndex + 3] = topRight;
355
356 var triIndex = i * triArraySizeRatio;
357 triangles[triIndex] = vertIndex; // bottomLeft
358 triangles[triIndex + 1] = vertIndex + 1; // topLeft
359 triangles[triIndex + 2] = vertIndex + 2; // bottomRight
360
361 triangles[triIndex + 3] = vertIndex + 2; // bottomRight
362 triangles[triIndex + 4] = vertIndex + 1; // topLeft
363 triangles[triIndex + 5] = vertIndex + 3; // topRight
364 }
365
366 meshRuler.vertices = verts;
367 meshRuler.triangles = triangles;
368 meshRuler.RecalculateNormals();
369 }
370
371 // Defines a series of line segments in gameboard space (-)
372 public void ResetGrid(ScaleSettings newScaleSettings = null, GameBoardSettings newGameBoardSettings = null)
373 {
374 if (newScaleSettings != null)
375 {
376 this.scaleSettings = newScaleSettings;
377 }
378 if(newGameBoardSettings != null)
379 {
380 this.gameBoardSettings = newGameBoardSettings;
381 }
382
383 if(xGridLines == null)
384 {
385 xGridLines = new List<LineSegment>();
386 }
387 else xGridLines.Clear();
388
389 if(zGridLines == null)
390 {
391 zGridLines = new List<LineSegment>();
392 }
393 else zGridLines.Clear();
394
395 var lineLengthZ = 0.5f;
396 var linePosOffsetZ = lineLengthZ - 0.5f;
397 var lineStartPosZ = lineLengthZ;
398 var lineStopPosZ = -lineLengthZ + linePosOffsetZ;
399 var minDimension = Mathf.Min(usableGameBoardWidthInMeters, usableGameBoardLengthInMeters);
400 var oneUnit = scaleSettings.oneUnitLengthInMeters / minDimension;
401 // Starting from the center outward, define x-axis grid lines in 1-unit increments.
402 for (int i = 0; i * scaleSettings.oneUnitLengthInMeters < usableGameBoardWidthInMeters / 2; i++)
403 {
404 float distanceFromCenter = i * (scaleSettings.oneUnitLengthInMeters / minDimension);
405 int lod = 1; // TODO: Change this later
406
407 xGridLines.Add(new LineSegment(new Vector3(distanceFromCenter, 0, lineStartPosZ), new Vector3(distanceFromCenter, 0, lineStopPosZ), lod));
408
409 // No need to draw a second overlapping pair of lines along the origin when i == 0.
410 if(i < 1) {continue;}
411
412 xGridLines.Add(new LineSegment(new Vector3(-distanceFromCenter, 0, lineStartPosZ), new Vector3(-distanceFromCenter, 0, lineStopPosZ), lod));
413 }
414
415 // Starting from the center outward, define z-axis grid lines in 1-unit increments.
416 for (int i = 0; i * scaleSettings.oneUnitLengthInMeters < usableGameBoardWidthInMeters / 2; i++)
417 {
418 float distanceFromCenter = i * (scaleSettings.oneUnitLengthInMeters / minDimension);
419 int lod = 1; // TODO: Change this later
420
421 zGridLines.Add(new LineSegment(new Vector3(0.5f, 0, distanceFromCenter), new Vector3(-0.5f, 0, distanceFromCenter), lod));
422
423 // No need to draw a second overlapping pair of lines along the origin when i == 0.
424 if(i < scaleSettings.oneUnitLengthInMeters) { continue; }
425
426 zGridLines.Add(new LineSegment(new Vector3(0.5f, 0, -distanceFromCenter), new Vector3(-0.5f, 0, -distanceFromCenter), lod));
427 }
428 }
429
430 public void Draw(ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings,
431 float alpha, bool showGrid, float gridOffsetY = 0f)
432 {
433 Configure (scaleSettings, gameBoardSettings, gridOffsetY, gameboardExtents);
434
435 if (null == gameBoardSettings) { return; }
436
437 GameboardMesh gameboardMesh;
438
439 switch (gameboardType)
440 {
441 case GameboardType.GameboardType_XE:
442 gameboardMesh = gameboardMesh_XE;
443 break;
444 case GameboardType.GameboardType_XE_Raised:
445 gameboardMesh = gameboardMesh_XE_Raised;
446 break;
447 default:
448 case GameboardType.GameboardType_LE:
449 case GameboardType.GameboardType_None:
450 gameboardMesh = gameboardMesh_LE;
451 break;
452 }
453
454 var rotToUSTAGE_UBOARD = GetRotToUSTAGE_UBOARD(gameBoardSettings);
455
456 Matrix4x4 mtxUWRLD_USTAGE = gameBoardSettings.currentGameBoard.transform.localToWorldMatrix * Matrix4x4.Scale(Vector3.one * scaleToUWRLD_USTAGE);
457
458 //------------------------------------------------------------------------------------------
459 // Draw the gameboard attached to the stage origin.
460 //------------------------------------------------------------------------------------------
461
462 // When the physical board orientation matches the stage orientation, the stage borders are redundant (and visually busy).
463 // We will hide the stage gizmo when the physical gameboard orientation is mostly aligned with the stage orientation (i.e. gravity).
464 // This is meant to gracefully handle the imperfect real world, in which tabletops might not be perfectly level.
465
466 // Start by defining some angular deflection thresholds.
467 var fadeInStartAngle = 3f; // Start fading in above 3 degrees of rotation
468 var fadeInEndAngle = 10f; // Finish fading in by 10 degrees of rotation
469 var stageGizmoAlpha = GetStageOpacity(rotToUSTAGE_UBOARD, fadeInStartAngle, fadeInEndAngle, alpha);
470
471 DrawStage(gameboardMesh, mtxUWRLD_USTAGE, stageGizmoAlpha);
472
473 //------------------------------------------------------------------------------------------
474 // Draw the physical gameboard orientation.
475 //------------------------------------------------------------------------------------------
476
477 // Determine which corner of the physical gameboard is lowest in stage space.
478 var lowestCorner_USTAGE = GetLowestCorner(rotToUSTAGE_UBOARD);
479
480 var verticalOffset_USTAGE = -lowestCorner_USTAGE.pos_USTAGE.y;
481 var mtxUSTAGE_UBOARD = Matrix4x4.TRS(verticalOffset_USTAGE * Vector3.up, rotToUSTAGE_UBOARD, Vector3.one);
482 var mtxUWRLD_UBOARD = mtxUWRLD_USTAGE * mtxUSTAGE_UBOARD;
483
484 DrawPhysicalBoard(gameboardMesh, mtxUWRLD_UBOARD, alpha);
485
486 if (showGrid)
487 {
488 DrawGrid(alpha);
489 DrawRulers(alpha);
490 }
491 }
492
493 private void DrawStage(GameboardMesh gameboardMesh, Matrix4x4 mtxGizmo, float alpha)
494 {
495 if (alpha <= 0f)
496 {
497 return;
498 }
499
500 using (new GizmoReferenceFrame(mtxGizmo))
501 {
502 if (gameboardMesh.meshStage != null)
503 {
504 using (new GizmoColor(1.0f, 1.0f, 1.0f, alpha))
505 {
506 Gizmos.DrawMesh(gameboardMesh.meshStage);
507 }
508 }
509
510 if (gameboardMesh.meshStageCorner != null)
511 {
512 using (new GizmoColor(t5Orange, alpha))
513 {
514 Gizmos.DrawMesh(gameboardMesh.meshStageCorner, 0);
515 }
516 }
517
518 if (gameboardMesh.meshSurface != null)
519 {
520 using (new GizmoColor(0.5f, 0.5f, 0.5f, alpha / 2f))
521 {
522 Gizmos.DrawMesh(gameboardMesh.meshSurface, 0);
523 }
524 }
525 }
526 }
527
528 private (GameBoard.Corner corner, Vector3 pos_USTAGE) GetLowestCorner(Quaternion rotToUSTAGE_UBOARD)
529 {
530 // Start by obtaining the extents of the current gameboard.
531 // We'll need them for obtaining corner postions in a moment.
532 T5_GameboardSize gameboardSize;
533
534 switch (gameboardType)
535 {
536 case GameboardType.GameboardType_XE:
537 gameboardSize = GameBoard.GameboardExtents.GAMEBOARD_SIZE_XE;
538 break;
539 case GameboardType.GameboardType_XE_Raised:
540 gameboardSize = GameBoard.GameboardExtents.GAMEBOARD_SIZE_XE_RAISED;
541 break;
542 case GameboardType.GameboardType_LE:
543 default:
544 gameboardSize = GameBoard.GameboardExtents.GAMEBOARD_SIZE_LE;
545 break;
546 }
547
548 GameBoard.GameboardExtents gameboardExtents = new GameBoard.GameboardExtents(gameboardSize);
549
550 // Determine which corner of the physical gameboard is lowest in stage space.
551 (GameBoard.Corner corner, Vector3 pos_USTAGE) lowestCorner_USTAGE
552 = (GameBoard.Corner.FarLeft, rotToUSTAGE_UBOARD * gameboardExtents.GetCornerPositionInGameboardSpace(GameBoard.Corner.FarLeft));
553
554 for (GameBoard.Corner corner = GameBoard.Corner.FarRight; (int)corner < 4; corner++)
555 {
556 var cornerPos_USTAGE = rotToUSTAGE_UBOARD * gameboardExtents.GetCornerPositionInGameboardSpace(corner);
557
558 if (cornerPos_USTAGE.y < lowestCorner_USTAGE.pos_USTAGE.y)
559 {
560 lowestCorner_USTAGE = (corner, cornerPos_USTAGE);
561 }
562 }
563 return lowestCorner_USTAGE;
564 }
565
566 private Quaternion GetRotToUSTAGE_UBOARD(GameBoardSettings gameboardSettings)
567 {
568 var rotToUSTAGE_UBOARD = gameboardSettings.enableGameboardOrientationOverride
569 ? Quaternion.Euler(gameboardSettings.gameboardOrientationOverride)
570 : gameboardSettings.physicalGameboardOrigin == null ? Quaternion.identity : gameboardSettings.physicalGameboardOrigin.transform.localRotation;
571 return rotToUSTAGE_UBOARD;
572 }
573
574 private float GetStageOpacity(Quaternion rotToULBOARD_ULSTAGE, float lowerAngleThreshold, float higherAngleThreshold, float baseOpacity)
575 {
576 /*
577 * A naive approach to doing this might be to compare the stage's normal vector (y-axis, gravity, etc)
578 * to the physical gameboard's normal, using the measured angular deflection to lerp between the angle thresholds.
579 * However, this approach does not account for yaw rotations about the y-axis.
580 * We're going to need to do this the right way and compare rotations using quaternions.
581 *
582 * Luckily, a quaternion is also a Vector4, so we can use dot products to check for rotations
583 * that are "aligned", "perpendicular", or "opposite", resulting in values of 1, 0, and -1 respectively.
584 * Subtle rotations like the ones we care about are still nearly-aligned with the default pose,
585 * so they will result in dot products close to 1.0 (or -1.0, which is covered below).
586 *
587 * To avoid converting back to degrees, we can just lerp between a couple reference rotations
588 * (based on the angle thresholds defined above) and their corresponding dot products with the default orientation.
589 *
590 * One gotcha to this approach is that we must use the absolute value of the dot product to handle rotations that are
591 * visually equivalent to the default rotation. This is because a 360 degree rotation about an arbitrary axis is equivalent
592 * to the "opposite" quaternion of the identity quaternion we started with, and the dot product result is -1.
593 * If left alone, this would fall outside the threshold range and result in the gizmo persisting at values near 360 degrees,
594 * then fading around 720 degrees, a pattern that would continue alternating the gizmo's visibility behavior in 360 degree increments.
595 * Mathematically this makes sense, but it defies intuition and confuses users without being relevant to the task at hand.
596 * Dropping the negative sign lets the border gizmo fade properly in response to all functionally equivalent rotations.
597 */
598 var alignmentWithStage = 1f - Mathf.Abs(Quaternion.Dot(Quaternion.identity, rotToULBOARD_ULSTAGE));
599 var referenceAlignmentStartThreshold = 1f - Quaternion.Dot(Quaternion.identity, Quaternion.Euler(Vector3.right * lowerAngleThreshold));
600 var referenceAlignmentEndThreshold = 1f - Quaternion.Dot(Quaternion.identity, Quaternion.Euler(Vector3.right * higherAngleThreshold));
601 return Mathf.Lerp(0f, baseOpacity,
602 Mathf.Min(Mathf.Max(alignmentWithStage - referenceAlignmentStartThreshold, 0f) / (referenceAlignmentEndThreshold - referenceAlignmentStartThreshold), 1f));
603 }
604
605 private void DrawPhysicalBoard(GameboardMesh gameboardMesh, Matrix4x4 mtxGizmo, float alpha)
606 {
607 if (alpha <= 0f)
608 {
609 return;
610 }
611
612 using (new GizmoReferenceFrame(mtxGizmo))
613 {
614 if (gameboardMesh.meshSurface != null)
615 {
616 using (new GizmoColor(0.5f, 0.5f, 0.5f, alpha))
617 {
618 Gizmos.DrawMesh(gameboardMesh.meshSurface, 0);
619 }
620 }
621
622 if (gameboardMesh.meshBorder != null)
623 {
624 using (new GizmoColor(0.0f, 0.0f, 0.0f, alpha))
625 {
626 Gizmos.DrawMesh(gameboardMesh.meshBorder, 0);
627 }
628 }
629
630 if (meshLogoBorder != null && meshLogoLeftCharacter != null && meshLogoRightCharacter != null)
631 {
632 DrawLogo(Gizmos.matrix, alpha);
633 }
634 }
635 }
636
637 private void DrawLogo(Matrix4x4 mtxGizmo, float alpha)
638 {
639 if (alpha <= 0f)
640 {
641 return;
642 }
643
644 using (new GizmoReferenceFrame(mtxGizmo))
645 {
646 using (new GizmoColor(t5LightGray, alpha))
647 {
648 Gizmos.DrawMesh( meshLogoBorder, 0 );
649 }
650
651 using (new GizmoColor(t5Orange, alpha))
652 {
653 Gizmos.DrawMesh(meshLogoLeftCharacter, 0);
654 }
655
656 using (new GizmoColor(t5Gray, alpha))
657 {
658 Gizmos.DrawMesh(meshLogoRightCharacter, 0);
659 }
660 }
661 }
662
663 private void DrawGrid(float alpha)
664 {
665 var contentScaleFactor = scaleToUWRLD_USTAGE;
666 float heightOffsetInMeters = yGridOffset * scaleSettings.oneUnitLengthInMeters;
667 float lengthOffsetInMeters = Mathf.Max(usableGameBoardLengthInMeters - usableGameBoardWidthInMeters, 0f) / 2f;
668
669 var gameBoardTransform = gameBoardSettings.currentGameBoard.transform;
670
671 // Define the transformations for the grid origin and orientation.
672 Matrix4x4 mtxPreTransform = Matrix4x4.Scale(
673 Vector3.one * Mathf.Min(usableGameBoardWidthInMeters, usableGameBoardLengthInMeters));
674 //new Vector3(usableGameBoardWidthInMeters, 1f, usableGameBoardLengthInMeters));
675 Matrix4x4 mtxGizmo = Matrix4x4.TRS(
676 gameBoardSettings.gameBoardCenter + (gameBoardTransform.up * heightOffsetInMeters / contentScaleFactor),
677 gameBoardTransform.rotation,
678 Vector3.one / contentScaleFactor ) * mtxPreTransform;
679
680 // Unless we're using inches, grid lines should appear in groups of 10.
681 var gridLinePeriod = scaleSettings.contentScaleUnit == LengthUnit.Inches ? 12 : 10;
682
683 // We want to fade out grid lines spaced closer than 10 pixels on the screen.
684 // We'll do two iterations of fading out.
685 float beginFadeThreshold = 20f;
686 float endFadeThreshold = 10f;
687
688 // To determine the spacing, we'll sample a position under the camera (even if it's off the board).
689 TryGetCameraPositionOverGameBoard(out var cameraPositionOverGameBoard);
690 Vector3 testUnitVector = cameraPositionOverGameBoard + Vector3.right * scaleSettings.oneUnitLengthInMeters / contentScaleFactor;
691 Vector3 secondTestUnitVector = cameraPositionOverGameBoard + Vector3.right * gridLinePeriod * scaleSettings.oneUnitLengthInMeters / contentScaleFactor;
692
693 var screenSpaceDistance = (SceneView.currentDrawingSceneView.camera.WorldToScreenPoint(testUnitVector)
694 - SceneView.currentDrawingSceneView.camera.WorldToScreenPoint(cameraPositionOverGameBoard)).magnitude;
695 var secondScreenSpaceDistance = (SceneView.currentDrawingSceneView.camera.WorldToScreenPoint(secondTestUnitVector)
696 - SceneView.currentDrawingSceneView.camera.WorldToScreenPoint(cameraPositionOverGameBoard)).magnitude;
697
698 // Check the sampled values against the thresholds we defined above and calculate transparency values.
699 var beginFade = screenSpaceDistance < beginFadeThreshold;
700 var endFade = screenSpaceDistance < endFadeThreshold;
701
702 var beginSecondFade = secondScreenSpaceDistance < beginFadeThreshold;
703 var endSecondFade = secondScreenSpaceDistance < endFadeThreshold;
704
705 var fadeFactor = 1f - Mathf.Clamp((beginFadeThreshold - screenSpaceDistance) / endFadeThreshold, 0f, 1f);
706 var secondFadeFactor = 1f - Mathf.Clamp((beginFadeThreshold - secondScreenSpaceDistance) / endFadeThreshold, 0f, 1f);
707
708 var fadeAlpha = alpha * fadeFactor;
709 var secondFadeAlpha = alpha * secondFadeFactor;
710
711 // Finally, draw some grid lines.
712 using (new GizmoReferenceFrame(mtxGizmo))
713 {
714 DrawGridLines(xGridLines, gridLinePeriod, alpha, beginFade, endFade, fadeAlpha, beginSecondFade, endSecondFade, secondFadeAlpha);
715 DrawGridLines(zGridLines, gridLinePeriod, alpha, beginFade, endFade, fadeAlpha, beginSecondFade, endSecondFade, secondFadeAlpha);
716 }
717 }
718
719 private void DrawGridLines(List<LineSegment> lines, int gridLinePeriod, float alpha, bool beginFade, bool endFade,
720 float fadeAlpha, bool beginSecondFade, bool endSecondFade, float secondFadeAlpha)
721 {
722 var gridLinePeriodSquared = gridLinePeriod * gridLinePeriod;
723 var mtxGizmo = Gizmos.matrix;
724
725 // We want to use a sphere around the projected cursor position to check for collision with lines on the grid.
726 // Any lines that collide will be colored differently. Set the sphere diameter to a single content unit, and scale it as the grid fades.
727 float cursorSphereRadius = scaleToUWRLD_USTAGE * (scaleSettings.oneUnitLengthInMeters / 2f);
728 if(endSecondFade)
729 {
730 cursorSphereRadius *= (gridLinePeriod * gridLinePeriod) / 2f;
731 }
732 else if(endFade)
733 {
734 cursorSphereRadius *= gridLinePeriod / 2f;
735 }
736
737 for(int i = 0; i < lines.Count; i++)
738 {
739 // Determine whether we should skip drawing this line.
740 var isPeriodic = i % gridLinePeriod == 0 || i % gridLinePeriod == gridLinePeriod - 1;
741 if(endFade && !isPeriodic) { continue; }
742
743 var isDoublePeriodic = i % gridLinePeriodSquared == 0 || i % gridLinePeriodSquared == gridLinePeriodSquared - 1;
744 if(endSecondFade && !isDoublePeriodic) { continue; }
745
746 // We'll need the current line defined in world space to check for cursor collision.
747 var currentGridLineSegment = lines[i];
748 var transformedLineSegment = new LineSegment(
749 mtxGizmo.MultiplyPoint3x4(currentGridLineSegment.Start),
750 mtxGizmo.MultiplyPoint3x4(currentGridLineSegment.End), currentGridLineSegment.LOD);
751
752 var lineCollidesWithCursor = TryGetCursorPosition(out var cursorPosition)
753 && CheckRaySphereCollision(cursorPosition, cursorSphereRadius, transformedLineSegment);
754
755 // Apply any fading required by the camera's distance from the grid.
756 var lineAlpha = alpha;
757 if(beginSecondFade && !isDoublePeriodic)
758 {
759 lineAlpha = secondFadeAlpha;
760 }
761 else if(beginFade && !isPeriodic)
762 {
763 lineAlpha = fadeAlpha;
764 }
765
766 using (lineCollidesWithCursor
767 ? new GizmoColor(1f, 0f, 0f, alpha)
768 : new GizmoColor(1f, 1f, 1f, lineAlpha))
769 {
770 // If the line collides with the cursor, project it on the edges of the board.
771 if (lineCollidesWithCursor)
772 {
773 var xLineExtensionFactor = totalGameBoardWidthInMeters / usableGameBoardWidthInMeters;
774 var zLineExtensionFactor = totalGameBoardLengthInMeters / usableGameBoardLengthInMeters;
775
776 // We need to know which direction to extend it. It's either x-axis aligned or z-axis aligned.
777 var lineDifference = currentGridLineSegment.End - currentGridLineSegment.Start;
778 var xAxisAligned = Mathf.Abs(Vector3.Dot(Vector3.right, lineDifference)) == 1;
779
780 var mtxPreTransform = Matrix4x4.Scale(
781 (xAxisAligned ? new Vector3(xLineExtensionFactor, 1f, 1f) : new Vector3(1f, 1f, zLineExtensionFactor)));
782 var newMtxWorld = Matrix4x4.TRS(
783 gameBoardSettings.currentGameBoard.transform.position,
784 gameBoardSettings.currentGameBoard.transform.rotation,
785 new Vector3(scaleToUWRLD_USTAGE * usableGameBoardWidthInMeters, 0f, scaleToUWRLD_USTAGE * usableGameBoardWidthInMeters));
786
787 // Finally, draw a line.
788 using (new GizmoReferenceFrame(newMtxWorld))
789 {
790 Gizmos.DrawLine(currentGridLineSegment.Start, mtxPreTransform.MultiplyPoint3x4(currentGridLineSegment.Start));
791 Gizmos.DrawLine(currentGridLineSegment.End, mtxPreTransform.MultiplyPoint3x4(currentGridLineSegment.End));
792 }
793 }
794 Gizmos.DrawLine(currentGridLineSegment.Start, currentGridLineSegment.End);
795 }
796 }
797 }
798
799 private bool TryGetCursorPosition(out Vector3 intersectPoint)
800 {
801 var sceneView = UnityEditor.SceneView.currentDrawingSceneView;
802 var sceneViewCamera = sceneView.camera;
803 var mousePos = Event.current.mousePosition - sceneView.position.position;
804
805 // Get the mouse position
806 mousePos = GUIUtility.GUIToScreenPoint(mousePos);
807
808 // The calls above tends to include some extra invisible space above the rendered scene view, so we compensate.
809 var frameOffset = Vector2.down * 36f;
810 mousePos.y = (sceneViewCamera.pixelHeight - mousePos.y - frameOffset.y);
811
812 var ray = sceneViewCamera.ScreenPointToRay(mousePos);
813
814 return TryGetGridPlanePosition(ray, out intersectPoint);
815 }
816
817 private bool TryGetCameraPositionOverGameBoard(out Vector3 intersectPoint)
818 {
819 var ray = new Ray(UnityEditor.SceneView.currentDrawingSceneView.camera.transform.position, -gameBoardSettings.currentGameBoard.transform.up);
820 return TryGetGridPlanePosition(ray, out intersectPoint);
821 }
822
823 private bool TryGetGridPlanePosition(Ray ray, out Vector3 intersectPoint)
824 {
825 // Set up the collision plane
826 var planeOffsetY = (yGridOffset * scaleSettings.oneUnitLengthInMeters) / scaleToUWRLD_USTAGE;
827 var gridPlane = new Plane(
828 gameBoardSettings.currentGameBoard.transform.up,
829 gameBoardSettings.currentGameBoard.transform.localToWorldMatrix.MultiplyPoint3x4(planeOffsetY * Vector3.up));
830
831 if(gridPlane.Raycast(ray, out var intersectionDistance))
832 {
833 // Get the point along the ray intersecting the plane.
834 intersectPoint = ray.GetPoint(intersectionDistance);
835 return true;
836 }
837
838 intersectPoint = Vector3.zero;
839 return false;
840 }
841
842 private bool CheckRaySphereCollision(Vector3 sphereCenter, float sphereRadius, LineSegment lineSegment){
843
844 var ray = new Ray(lineSegment.Start, lineSegment.End - lineSegment.Start);
845
846 Vector3 rayToSphereCenterDistance = ray.origin - sphereCenter;
847 float a = Vector3.Dot(ray.direction, ray.direction);
848 float b = 2f * Vector3.Dot(rayToSphereCenterDistance, ray.direction);
849 float c = Vector3.Dot(rayToSphereCenterDistance, rayToSphereCenterDistance) - sphereRadius * sphereRadius;
850 float discriminant = (b * b) - (4*a*c);
851 return discriminant > 0;
852 }
853
854 private void DrawRulers(float alpha)
855 {
856 if(meshRuler == null) { return; }
857
858 var contentScaleFactor = scaleToUWRLD_USTAGE;
859 float borderWidthInMeters = GameBoard.GameboardExtents.BORDER_WIDTH_IN_METERS;
860 float lengthOffsetInMeters = Mathf.Max(usableGameBoardLengthInMeters - usableGameBoardWidthInMeters, 0f) / 2f;
861 var gameBoardTransform = gameBoardSettings.currentGameBoard.transform;
862 var gameboardCenter = gameBoardSettings.gameBoardCenter;
863
864 Matrix4x4 mtxPreTransform = Matrix4x4.Scale(new Vector3(usableGameBoardWidthInMeters, 1f, usableGameBoardLengthInMeters));
865
866 using (new GizmoColor(1f, 0.8320962f, 0.3803922f, alpha))
867 {
868 // Far edge
869 Matrix4x4 mtxWorld = Matrix4x4.TRS(gameboardCenter + (gameBoardTransform.forward * lengthOffsetInMeters / contentScaleFactor) + (0.5f * (usableGameBoardLengthInMeters + borderWidthInMeters) * gameBoardTransform.forward) / contentScaleFactor,
870 Quaternion.Euler(gameBoardSettings.gameBoardRotation),
871 Vector3.one / contentScaleFactor );
872 using (new GizmoReferenceFrame(mtxWorld * mtxPreTransform))
873 {
874 Gizmos.DrawMesh(meshRuler, 0 );
875 }
876
877 // Near edge
878 mtxWorld = Matrix4x4.TRS(gameboardCenter - (0.5f * (usableGameBoardWidthInMeters + borderWidthInMeters) * gameBoardTransform.forward) / contentScaleFactor,
879 Quaternion.Euler(gameBoardSettings.gameBoardRotation),
880 Vector3.one / contentScaleFactor );
881 using (new GizmoReferenceFrame(mtxWorld * mtxPreTransform))
882 {
883 Gizmos.DrawMesh(meshRuler, 0 );
884 }
885
886 // Right edge
887 mtxPreTransform = Matrix4x4.TRS( Vector3.zero, Quaternion.Euler(0.0f, 90.0f, 0.0f), Vector3.one * usableGameBoardWidthInMeters);
888 mtxWorld = Matrix4x4.TRS(gameboardCenter + (0.5f * (usableGameBoardWidthInMeters + borderWidthInMeters) * gameBoardTransform.right) / contentScaleFactor,
889 Quaternion.Euler(gameBoardSettings.gameBoardRotation),
890 Vector3.one / contentScaleFactor );
891 using (new GizmoReferenceFrame(mtxWorld * mtxPreTransform))
892 {
893 Gizmos.DrawMesh(meshRuler, 0 );
894 }
895
896 // Left Edge
897 mtxWorld = Matrix4x4.TRS(gameboardCenter - (0.5f * (usableGameBoardWidthInMeters + borderWidthInMeters) * gameBoardTransform.right) / contentScaleFactor,
898 Quaternion.Euler(gameBoardSettings.gameBoardRotation),
899 Vector3.one / contentScaleFactor );
900 using (new GizmoReferenceFrame(mtxWorld * mtxPreTransform))
901 {
902 Gizmos.DrawMesh(meshRuler, 0 );
903 }
904 }
905 }
906 }
907}
908#endif
GameboardType
The type of Gameboard being tracked by the glasses.