Spelunx Cavern SDK
 
Loading...
Searching...
No Matches
CavernRenderer.cs
Go to the documentation of this file.
1using UnityEngine;
2using UnityEngine.Rendering;
3using UnityEngine.Rendering.Universal;
4using System.Collections.Generic;
5using UnityEngine.Events;
6
7
8namespace Spelunx
9{
10 [ExecuteInEditMode]
11 public class CavernRenderer : MonoBehaviour
12 {
13 public enum StereoscopicMode
14 {
15 Mono, // Monoscopic mode. No 3D effect.
16 Stereo, // Stereoscopic mode. Gives a 3D-movie effect when wearing 3D glasses.
17 }
18
20 {
21 VeryLow = 512,
22 Low = 1024,
23 Mid = 2048,
24 High = 4096,
25 VeryHigh = 8192,
26 }
27
28 public enum PreviewEye { Left, Right }
29
30 private enum CubemapIndex
31 {
32 North = 0, // Also used for monoscopic.
33 South,
34 East,
35 West,
36
37 Num,
38 }
39
40 [Header("Camera Settings")]
41 /// Stereoscopic mode to render the
42 [SerializeField] private StereoscopicMode stereoMode = StereoscopicMode.Mono;
43 [SerializeField] private CubemapResolution cubemapResolution = CubemapResolution.Mid;
44 /// Interpupillary Distance (IPD) in metres.
45 [SerializeField, Range(0.05f, 0.08f)] private float interpupillaryDistance = 0.065f;
46 /// Cavern physical screen height in metres.
47 [SerializeField, Min(0.1f)] private float cavernHeight = 2.0f;
48 /// Cavern physical screen radius in metres.
49 [SerializeField, Min(0.1f)] private float cavernRadius = 3.0f;
50 /// Cavern physical screen angle in degrees.
51 [SerializeField, Range(1.0f, 360.0f)] private float cavernAngle = 270.0f;
52 /// Cavern physical screen elevation in metres, relative to the player's feet.
53 [SerializeField, Range(-0.5f, 0.5f)] private float cavernElevation = 0.0f;
54 /// Increase accuracy at the cost of significant performance.
55 [SerializeField] private bool enableConvergence = false;
56 /// Software support for swapping the left and right eyes. (Off - Left Eye On Top, On - Right Eye On Top)
57 [SerializeField] private bool swapEyes = false;
58
59 [Header("Head Tracking")]
60 /// If set to true, the ear will follow the head.
61 [SerializeField] private bool tetherEar = true;
62 /// If set to true, the head position will be clamped to within the the radius of the screen.
63 [SerializeField] private bool clampHeadPosition = true;
64 /// <summary>
65 /// Sets the clamping radius of the head, if clampHeadPosition = true.
66 /// For example, if clampHeadRatio = 0.8 and cavernRadius = 3, the head will be clamped to a radius of 2.4.
67 /// </summary>
68 [SerializeField, Range(0.0f, 1.0f)] private float clampHeadRatio = 0.9f;
69
70 [Header("Preview")]
71 [SerializeField] private CubemapResolution previewResolution = CubemapResolution.VeryLow;
72 [SerializeField] private PreviewEye previewEye = PreviewEye.Left;
73 [SerializeField, Tooltip("Should the CAVERN preview live update?")] private bool livePreview = true;
74
75 [Header("References (Do NOT edit!)")]
76 [SerializeField] private Transform head;
77 [SerializeField] private Camera eye; // Ensure that UI culling mask is unset. Ensure that Output > Target Eye is set to None in the Inspector, or it'll render a blank screen on the Cavern PC! No I don't know why.
78 [SerializeField] private Camera guiCamera; // Ensure that ONLY UI culling mask is set. Ensure that Output > Target Eye is set to None in the Inspector, or it'll render a blank screen on the Cavern PC! No I don't know why.
79 [SerializeField] private AudioListener ear;
80 [SerializeField] private Shader shader;
81 [SerializeField] private Material previewMaterial;
82
83 // Internal variables.
84 private Material material = null;
85 private RenderTexture[] cubemaps = null;
86 private Mesh previewMesh = null;
87 private RenderTexture previewTexture = null;
88 private CavernRenderPass cavernRenderPass;
89
90 [HideInInspector]
91 public UnityEvent settingsChanged;
92
93 public CubemapResolution GetCubemapResolution() { return cubemapResolution; }
94 public StereoscopicMode GetStereoscopicMode() { return stereoMode; }
95 public float IPD
96 {
97 get => interpupillaryDistance;
98 set
99 {
100 if (0.05f <= value && 0.08f >= value)
101 {
102
103 interpupillaryDistance = value;
104 }
105 }
106 }
107 public float GetCavernHeight() { return cavernHeight; }
108 public float GetCavernRadius() { return cavernRadius; }
109 public float GetCavernAngle() { return cavernAngle; }
110 public float GetCavernElevation() { return cavernElevation; }
111 public float GetAspectRatio() { return ((cavernAngle / 360.0f) * Mathf.PI * cavernRadius * 2.0f) / cavernHeight; }
112 public GameObject GetHead() { return head.gameObject; }
113 public GameObject GetEye() { return eye.gameObject; }
114 public GameObject GetEar() { return ear.gameObject; }
115 public GameObject GetGUICamera() { return guiCamera.gameObject; }
116
118 {
119 stereoMode = mode;
120 }
121
122 public bool SwapEyes
123 {
124 get
125 {
126 return swapEyes;
127 }
128 set
129 {
130 swapEyes = value;
131 }
132 }
133
134 private void OnEnable()
135 {
136 RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
137 RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
138#if UNITY_EDITOR
139 UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
140 UnityEditor.EditorApplication.delayCall += OnEditorDelayCall;
141#endif
142 }
143
144 private void OnDisable()
145 {
146 RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
147 RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
148#if UNITY_EDITOR
149 UnityEditor.SceneManagement.EditorSceneManager.sceneSaved -= OnSceneSaved;
150 UnityEditor.EditorApplication.delayCall -= OnEditorDelayCall;
151#endif
152 }
153
154 private void Awake()
155 {
156 CreateCubemaps();
157 CreateMaterial();
158 CreatePreviewMesh();
159 CreatePreviewTexture();
160 cavernRenderPass = new CavernRenderPass(material);
161 }
162
163 private void Start()
164 {
165 // Since we are using the eye to render to cubemaps, we want to disable it here, so that it
166 // doesn't do a "normal" render to the screen, which will be a waste since we are overriding it.
167 // Instead, we will "highjack" the GUI camera insert a render pass into the URP RenderGraph to render the eye to the screen.
168 eye.enabled = false;
169 }
170
171 private void Update()
172 {
173
174 // If clampHeadPosition is true, limit the head position to be within the bounds of the circle.
175 if (clampHeadPosition)
176 {
177 Vector2 horizontalPosition = new Vector2(head.transform.localPosition.x, head.transform.localPosition.z);
178 if (horizontalPosition.sqrMagnitude > clampHeadRatio * clampHeadRatio * cavernRadius * cavernRadius)
179 {
180 horizontalPosition = horizontalPosition.normalized * clampHeadRatio * cavernRadius;
181 head.transform.localPosition = new Vector3(horizontalPosition.x, head.transform.localPosition.y, horizontalPosition.y);
182 }
183 }
184
185 if (tetherEar)
186 {
187 ear.gameObject.transform.position = head.transform.position;
188 ear.gameObject.transform.rotation = head.transform.rotation;
189 }
190
191#if UNITY_EDITOR
192 // Only render if we are playing and or showing a live preview.
193 if (UnityEditor.EditorApplication.isPlaying || livePreview)
194 {
195 RenderEyes();
196 if (previewTexture != null && material != null)
197 {
198 Graphics.Blit(null, previewTexture, material);
199 }
200 }
201#else
202 RenderEyes();
203#endif
204 }
205
206 // Find out which faces of the cubemaps should be rendered. We want the minimum number of faces to reduce the rendering workload.
207 // General approach: For front, back, left and right faces, look at the Cavern from the top-down view, so that it looks like a circle.
208 // "Slice" the circle into 4 quadrants using 2 lines that form an X, with the player's head being the intersection of the 2 lines.
209 // Then for each quadrant, determine which faces of each cubemap can be seen. Those are the faces we want to render.
210 private void GetRenderFaces(out int monoMask, out int northMask, out int southMask, out int eastMask, out int westMask)
211 {
212 // These are the built in bitmasks for Unity's cubemap faces.
213 const int rightMask = 1 << (int)CubemapFace.PositiveX;
214 const int leftMask = 1 << (int)CubemapFace.NegativeX;
215 const int topMask = 1 << (int)CubemapFace.PositiveY;
216 const int bottomMask = 1 << (int)CubemapFace.NegativeY;
217 const int frontMask = 1 << (int)CubemapFace.PositiveZ;
218 const int backMask = 1 << (int)CubemapFace.NegativeZ;
219
220 // Let's initalise all the output to 0.
221 monoMask = 0; northMask = 0; southMask = 0; eastMask = 0; westMask = 0;
222
223 Vector3 headPosition = head.transform.localPosition;
224
225 /*
226 Imagine this circle to be the Cavern screen. (Let's use a complete circle because this function should
227 generalise to a circle of any angle, even though the Cavern is only 270 degrees.)
228 , - ~ ~ ~ - ,
229 , ' ' ,
230 , ,
231 , ,
232 , ,
233 , ,
234 , ,
235 , ,
236 , ,
237 , , '
238 ' - , _ _ _ , '
239
240 // Now we want to "slice" the circle. I know my ASCII art is terrible, bear with me.
241 // I put all my skill points into programming and have none left for art.
242
243 \ /
244 North-West Boundary -> , - ~ ~ \ - , /
245 , ' \ / , <- North-East Boundary
246 , \ / ,
247 , O , <- The intersection of the 2 lines is the head position. It can be off-centre.
248 , / \ ,
249 , / \ ,
250 , / \ ,
251 , / \ ,
252 , / \, <- South-East Boundary
253 , / , '\
254 South-West Boundary ->' - , _/_ _ , ' \
255 / \
256
257 // The circle is sliced into 4 quadrants, each being 90 degrees. (The ASCII art is not to scale. Just pretend it is.)
258 // The places where the straight lines intersect with the circle are called boundaries (becauses I couldn't come up with a better name).
259 */
260 Vector3 southWestBoundary = Vector3.zero;
261 Vector3 northEastBoundary = Vector3.zero;
262 Vector3 northWestBoundary = Vector3.zero;
263 Vector3 southEastBoundary = Vector3.zero;
264
265 /*
266 To find the boundaries, just remember our secondary school linear algebra.
267 Note that our cubemaps are always taken from the head's position, we take the head to always be at (0, 0).
268 Instead, we "move" the screen by -HeadPosition,
269
270 Let (a, b) be the centre of the circle.
271 Circle Equation: (x - a)^2 + (y - b)^2 = r^2. ---- (1)
272 South West to North East Line Equation: y = x ---- (2)
273 North West to South East Line Equation: y = -x ---- (3)
274
275 Substitute (2) into (1):
276 (x - a)^2 + (x - b)^2 = r^2
277 x^2 - x(a + b) - 0.5(r^2 - a^2 - b^2) = 0
278 Solve this quadratic equation to get our intersection points for the South-West to North-East line and the circle.
279
280 Substitute (3) into (1):
281 (x - a)^2 + (-x - b)^2 = r^2
282 x^2 - x(a - b) - 0.5(r^2 - a^2 - b^2) = 0
283 Solve this quadratic equation to get our intersection points for the North-West to South-East line and the circle.
284 */
285 // Get North-East and South-West boundaries where the sampled cubemap switches for stereoscopic rendering.
286 List<float> xIntersectSouthWestToNorthEast = MathsUtil.SolveQuadraticEquation(
287 1.0f,
288 headPosition.x + headPosition.z,
289 -0.5f * (cavernRadius * cavernRadius - headPosition.x * headPosition.x - headPosition.z * headPosition.z));
290
291 // If there is only one solution to the quadratic equation, then there is only 1 point of intersection.
292 if (xIntersectSouthWestToNorthEast.Count == 1)
293 {
294 northEastBoundary = new Vector3(xIntersectSouthWestToNorthEast[0], 0.0f, xIntersectSouthWestToNorthEast[0]);
295 southWestBoundary = new Vector3(xIntersectSouthWestToNorthEast[0], 0.0f, xIntersectSouthWestToNorthEast[0]);
296 }
297 // Else there are 2 points of intersection.
298 else if (xIntersectSouthWestToNorthEast.Count == 2)
299 {
300 northEastBoundary = new Vector3(xIntersectSouthWestToNorthEast[1], 0.0f, xIntersectSouthWestToNorthEast[1]);
301 southWestBoundary = new Vector3(xIntersectSouthWestToNorthEast[0], 0.0f, xIntersectSouthWestToNorthEast[0]);
302 }
303
304 // Get North-West and South-East boundaries where the sampled cubemap switches for stereoscopic rendering.
305 List<float> xIntersectNorthWestToSouthEast = MathsUtil.SolveQuadraticEquation(
306 1.0f,
307 headPosition.x - headPosition.z,
308 -0.5f * (cavernRadius * cavernRadius - headPosition.x * headPosition.x - headPosition.z * headPosition.z));
309 if (xIntersectNorthWestToSouthEast.Count == 1)
310 {
311 northWestBoundary = new Vector3(xIntersectNorthWestToSouthEast[0], 0.0f, -xIntersectNorthWestToSouthEast[0]);
312 southEastBoundary = new Vector3(xIntersectNorthWestToSouthEast[0], 0.0f, -xIntersectNorthWestToSouthEast[0]);
313 }
314 else if (xIntersectNorthWestToSouthEast.Count == 2)
315 {
316 northWestBoundary = new Vector3(xIntersectNorthWestToSouthEast[0], 0.0f, -xIntersectNorthWestToSouthEast[0]);
317 southEastBoundary = new Vector3(xIntersectNorthWestToSouthEast[1], 0.0f, -xIntersectNorthWestToSouthEast[1]);
318 }
319
320 // For edge cases, assume that the top and bottom faces are not visible.
321 // It should be correct for most cases if the Cavern has sane dimensions.
322
323 // Edge Case 1: Head is moved out of the screen area and there are no intersects.
324 // This means that the screen is entirely in one quadrant relative to the head.
325 if (xIntersectSouthWestToNorthEast.Count == 0 && xIntersectNorthWestToSouthEast.Count == 0)
326 {
327 /*
328 \ /
329 \ /
330 \ /
331 O <- Head (Not to scale.)
332 / \
333 / \
334 / \
335 --
336 | | <- Screen (Not to scale.)
337 --
338 */
339 // Screen is entirely south of the head.
340 if (0.0f < headPosition.z &&
341 Mathf.Abs(headPosition.x) < Mathf.Abs(headPosition.z))
342 {
343 monoMask |= backMask;
344 eastMask |= backMask;
345 westMask |= backMask;
346 return;
347 }
348
349 /*
350 --
351 | | <- Screen (Not to scale.)
352 --
353 \ /
354 \ /
355 \ /
356 O <- Head (Not to scale.)
357 / \
358 / \
359 / \
360 */
361 // Screen is entirely north of the head.
362 if (headPosition.z < 0.0f &&
363 Mathf.Abs(headPosition.x) < Mathf.Abs(headPosition.z))
364 {
365 monoMask |= frontMask;
366 eastMask |= frontMask;
367 westMask |= frontMask;
368 return;
369 }
370
371 // Screen is entirely east of the head. (No more drawings, you should get the point by now.)
372 if (headPosition.x < 0.0f &&
373 Mathf.Abs(headPosition.z) < Mathf.Abs(headPosition.x))
374 {
375 monoMask |= rightMask;
376 northMask |= rightMask;
377 southMask |= rightMask;
378 return;
379 }
380
381 // Screen is entirely west of the head.
382 if (headPosition.x > 0.0f &&
383 Mathf.Abs(headPosition.z) < Mathf.Abs(headPosition.x))
384 {
385 monoMask |= leftMask;
386 northMask |= leftMask;
387 southMask |= leftMask;
388 return;
389 }
390 }
391
392 // Edge Case 2: Head is moved out of the screen and only the South-West to North-East line intersects.
393 if (xIntersectSouthWestToNorthEast.Count > 0 && xIntersectNorthWestToSouthEast.Count == 0)
394 {
395 /*
396 \ /
397 \ /
398 \ /
399 O <- Head (Not to scale.)
400 / \
401 / \
402 / \
403 /
404 --
405 | / | <- Screen (Not to scale.)
406 /--
407 /
408 */
409 // Screen is entirely south-west of the head.
410 if (Vector3.Dot(new Vector3(1.0f, 1.0f), new Vector2(headPosition.x, headPosition.z)) > 1.0f)
411 {
412 monoMask |= (backMask | leftMask);
413 eastMask |= backMask;
414 westMask |= backMask;
415 northMask |= leftMask;
416 southMask |= leftMask;
417 return;
418 }
419
420 /*
421 /
422 --
423 | / | <- Screen (Not to scale.)
424 /--
425 /
426 \ /
427 \ /
428 \ /
429 O <- Head (Not to scale.)
430 / \
431 / \
432 / \
433 */
434 // Screen is entirely north-east of the head.
435 if (Vector3.Dot(new Vector3(-1.0f, -1.0f), new Vector2(headPosition.x, headPosition.z)) > 1.0f)
436 {
437 monoMask = (frontMask | rightMask);
438 eastMask |= frontMask;
439 westMask |= frontMask;
440 northMask |= rightMask;
441 southMask |= rightMask;
442 return;
443 }
444 }
445
446 // Edge Case 3: Head is moved out of the screen and only the North-West to South-East line intersects.
447 if (xIntersectSouthWestToNorthEast.Count == 0 && xIntersectNorthWestToSouthEast.Count > 0)
448 {
449 // Screen is entirely north-west of the head. (Imagine the above drawings but for the North-West to South-East line.)
450 if (Vector3.Dot(new Vector3(1.0f, -1.0f), new Vector2(headPosition.x, headPosition.z)) > 1.0f)
451 {
452 monoMask = (frontMask | leftMask);
453 eastMask |= frontMask;
454 westMask |= frontMask;
455 northMask |= leftMask;
456 southMask |= leftMask;
457 return;
458 }
459
460 // Screen is entirely south-east of the head.
461 if (Vector3.Dot(new Vector3(-1.0f, 1.0f), new Vector2(headPosition.x, headPosition.z)) > 1.0f)
462 {
463 monoMask = (backMask | rightMask);
464 eastMask |= backMask;
465 westMask |= backMask;
466 northMask |= rightMask;
467 southMask |= rightMask;
468 return;
469 }
470 }
471
472 // Regular Case: The head is within the screen area.
473 // Take note that if we want more accurate rendering, that is to have the 2 eyes converge, more faces need to be rendered.
474 // Personally I don't notice much difference in terms of accuracy in real world experience, but it does cost quite a bit of performance.
475 // Therefore I added a toggle for it, and set it to false by default.
476 float screenTop = cavernElevation + cavernHeight - headPosition.y;
477 float screenBottom = cavernElevation - headPosition.y;
478 Vector3 headOffset = new Vector3(headPosition.x, 0.0f, headPosition.z);
479
480 /******************* Looking North *******************/
481 monoMask |= frontMask;
482 // For the enableConvergence = true case, much of the face cannot be seen. In the future, we can consider using a stencil buffer to disable rendering on the unseen portions of the face.
483 westMask |= frontMask | (enableConvergence ? rightMask : 0); // Left Eye
484 eastMask |= frontMask | (enableConvergence ? leftMask : 0); // Right Eye
485
486 /******************* Looking South *******************/
487 if (Vector3.Angle(headOffset + southWestBoundary, Vector3.forward) < cavernAngle * 0.5f ||
488 Vector3.Angle(headOffset + southEastBoundary, Vector3.forward) < cavernAngle * 0.5f)
489 {
490 monoMask |= backMask;
491 eastMask |= backMask; // Left Eye, no need to account for convergence because that is already handled in 'Looking North'.
492 westMask |= backMask; // Right Eye, no need to account for convergence because that is already handled in 'Looking North'.
493 }
494
495 /******************* Looking East *******************/
496 if (Vector3.Angle(headOffset + northEastBoundary, Vector3.forward) < cavernAngle * 0.5f ||
497 Vector3.Angle(headOffset + southEastBoundary, Vector3.forward) < cavernAngle * 0.5f)
498 {
499 monoMask |= rightMask;
500 northMask |= rightMask | (enableConvergence ? backMask : 0); // Left Eye
501 southMask |= rightMask | (enableConvergence ? frontMask : 0); // Right Eye
502 }
503
504 /******************* Looking West *******************/
505 if (Vector3.Angle(headOffset + northWestBoundary, Vector3.forward) < cavernAngle * 0.5f ||
506 Vector3.Angle(headOffset + southWestBoundary, Vector3.forward) < cavernAngle * 0.5f)
507 {
508 monoMask |= leftMask;
509 southMask |= leftMask | (enableConvergence ? frontMask : 0); // Left Eye
510 northMask |= leftMask | (enableConvergence ? backMask : 0); // Right Eye
511 }
512
513 /******************* Top & Bottom Faces *******************/
514 /*
515 Cubemap rendered from eye's position:
516 ----------------------------------
517 | |
518 | |
519 | |
520 | |
521 | |
522 | |
523 | (-0-) |
524 | |
525 | |
526 | |
527 | |
528 | |
529 | |
530 ----------------------------------
531
532 Find the horizontal distance from the eye to the screen.
533 Find the vertical distance from the eye to the top of the screen.
534 ^| (Top of Screen)
535 / |
536 / |
537 / |
538 / |
539 / |
540 / |
541 / | Vertical
542 / |
543 / |
544 / |
545 / |
546 v |
547 (-0-) <----------->O
548 Horizontal |
549 ^ |
550 | |
551 Badly Drawn Eye |
552 |
553 |
554 |
555 |
556
557 Top of cubemap needs to be rendered if the horizontal distance is smaller than the vertical distance:
558 ^|
559 / |
560 / |
561 / |
562 / |
563 / |
564 --------------------------/-------
565 | / |
566 | / |
567 | / |
568 | / |
569 | / |
570 | v |
571 | (-0-) <----------->O
572 | |
573 | |
574 | |
575 | |
576 | |
577 | |
578 ----------------------------------
579
580
581 Find the horizontal distance from the eye to the screen.
582 Find the vertical distance from the eye to the bottom of the screen. (May be different from the distance to the top of the screen!)
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 Horizontal |
596 (-0-) <----------->O
597 ^ |
598 ^ \ |
599 | \ | Vertical
600 Badly Drawn Eye |
601 \ |
602 \ |
603 \ |
604 v|
605
606 Bottom of cubemap needs to be rendered if the horizontal distance is smaller than the vertical distance:
607 |
608 |
609 |
610 |
611 |
612 |
613 ----------------------------------
614 | |
615 | |
616 | |
617 | |
618 | |
619 | Horizontal |
620 | (-0-) <----------->O
621 | ^ |
622 | \ |
623 | \ | Vertical
624 | \ |
625 | \ |
626 | \ |
627 ------------------------------\--|
628 v|
629
630 */
631 if (Mathf.Abs(northEastBoundary.z) < Mathf.Abs(screenTop) || // Looking North
632 Mathf.Abs(northWestBoundary.z) < Mathf.Abs(screenTop) || // Looking North
633 Mathf.Abs(southEastBoundary.z) < Mathf.Abs(screenTop) || // Looking South
634 Mathf.Abs(southWestBoundary.z) < Mathf.Abs(screenTop))
635 { // Looking South
636 monoMask |= topMask;
637 eastMask |= topMask;
638 westMask |= topMask;
639 }
640 if (Mathf.Abs(northEastBoundary.z) < Mathf.Abs(screenBottom) || // Looking North
641 Mathf.Abs(northWestBoundary.z) < Mathf.Abs(screenBottom) || // Looking North
642 Mathf.Abs(southEastBoundary.z) < Mathf.Abs(screenBottom) || // Looking South
643 Mathf.Abs(southWestBoundary.z) < Mathf.Abs(screenBottom))
644 { // Looking South
645 monoMask |= bottomMask;
646 eastMask |= bottomMask;
647 westMask |= bottomMask;
648 }
649 if (Mathf.Abs(northEastBoundary.x) < Mathf.Abs(screenTop) || // Looking East
650 Mathf.Abs(southEastBoundary.x) < Mathf.Abs(screenTop) || // Looking East
651 Mathf.Abs(northWestBoundary.x) < Mathf.Abs(screenTop) || // Looking West
652 Mathf.Abs(southWestBoundary.x) < Mathf.Abs(screenTop))
653 { // Looking West
654 monoMask |= topMask;
655 northMask |= topMask;
656 southMask |= topMask;
657 }
658 if (Mathf.Abs(northEastBoundary.x) < Mathf.Abs(screenBottom) || // Looking East
659 Mathf.Abs(southEastBoundary.x) < Mathf.Abs(screenBottom) || // Looking East
660 Mathf.Abs(northWestBoundary.x) < Mathf.Abs(screenBottom) || // Looking West
661 Mathf.Abs(southWestBoundary.x) < Mathf.Abs(screenBottom))
662 { // Looking West
663 monoMask |= bottomMask;
664 northMask |= bottomMask;
665 southMask |= bottomMask;
666 }
667 }
668
669 private void RenderEyes()
670 {
671 // Use Camera.MonoOrStereoscopicEye.Left or Camera.MonoOrStereoscopicEye.Right to ensure that the cubemap follows the camera's rotation.
672 // Camera.MonoOrStereoscopicEye.Mono renders the cubemap to be aligned to the world's axes instead.
673 int monoMask = 0; int northMask = 0; int southMask = 0; int eastMask = 0; int westMask = 0;
674 GetRenderFaces(out monoMask, out northMask, out southMask, out eastMask, out westMask);
675 switch (stereoMode)
676 {
677 case StereoscopicMode.Mono:
678 eye.stereoSeparation = 0.0f;
679 eye.transform.rotation = gameObject.transform.rotation; // Set eye's global orientation to the screen's orientation, regardless of the head's orientation.
680 eye.transform.localPosition = Vector3.zero;
681 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.North], monoMask, Camera.MonoOrStereoscopicEye.Left);
682 break;
683 case StereoscopicMode.Stereo:
684 eye.stereoSeparation = 0.0f;
685 eye.transform.rotation = gameObject.transform.rotation; // Set eye's global orientation to the screen's orientation, regardless of the head's orientation.
686 eye.transform.localPosition = new Vector3(0.0f, 0.0f, interpupillaryDistance * 0.5f);
687 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.North], northMask, Camera.MonoOrStereoscopicEye.Left);
688 eye.transform.localPosition = new Vector3(0.0f, 0.0f, interpupillaryDistance * -0.5f);
689 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.South], southMask, Camera.MonoOrStereoscopicEye.Right);
690 eye.transform.localPosition = new Vector3(interpupillaryDistance * 0.5f, 0.0f, 0.0f);
691 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.East], eastMask, Camera.MonoOrStereoscopicEye.Right);
692 eye.transform.localPosition = new Vector3(interpupillaryDistance * -0.5f, 0.0f, 0.0f);
693 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.West], westMask, Camera.MonoOrStereoscopicEye.Left);
694 eye.transform.localPosition = Vector3.zero;
695 break;
696 }
697
698 // Cavern Dimensions Uniforms
699 material.SetFloat("_CavernHeight", cavernHeight);
700 material.SetFloat("_CavernRadius", cavernRadius);
701 material.SetFloat("_CavernAngle", cavernAngle);
702 material.SetFloat("_CavernElevation", cavernElevation);
703
704 // Head Tracking Uniforms
705 material.SetVector("_HeadPosition", head.transform.localPosition);
706
707 // Stereoscopic Rendering Uniforms
708 material.SetInteger("_EnableStereoscopic", stereoMode == StereoscopicMode.Stereo ? 1 : 0);
709 material.SetInteger("_EnableConvergence", enableConvergence ? 1 : 0);
710 material.SetFloat("_InterpupillaryDistance", interpupillaryDistance);
711 material.SetInteger("_SwapEyes", swapEyes ? 1 : 0);
712 }
713
714 private void CreateCubemaps()
715 {
716 cubemaps = new RenderTexture[(int)CubemapIndex.Num];
717 for (int i = 0; i < (int)CubemapIndex.Num; ++i)
718 {
719 cubemaps[i] = new RenderTexture((int)cubemapResolution, (int)cubemapResolution, 32, RenderTextureFormat.ARGB32);
720 cubemaps[i].dimension = TextureDimension.Cube;
721 cubemaps[i].wrapMode = TextureWrapMode.Clamp;
722 }
723 }
724
725 private void CreateMaterial()
726 {
727 material = new Material(shader);
728 material.SetTexture("_CubemapNorth", cubemaps[(int)CubemapIndex.North]);
729 material.SetTexture("_CubemapSouth", cubemaps[(int)CubemapIndex.South]);
730 material.SetTexture("_CubemapEast", cubemaps[(int)CubemapIndex.East]);
731 material.SetTexture("_CubemapWest", cubemaps[(int)CubemapIndex.West]);
732 cavernRenderPass?.SetMaterial(material);
733 }
734
735 // generates a mesh used for the preview or other uses
736 public Mesh GenerateMesh()
737 {
738 Mesh mesh = new Mesh();
739 // Have about one panel every 10 degrees. A reasonable number.
740 int numPanels = Mathf.Max(1, (int)(cavernAngle / 10.0f));
741 int numVertices = (numPanels + 1) * 2;
742
743 Vector3[] positions = new Vector3[numVertices];
744 Vector3[] normals = new Vector3[numVertices];
745 Vector2[] uvs = new Vector2[numVertices];
746 int[] indices = new int[numPanels * 6];
747
748 /********************************************** Generate inner surface. **********************************************/
749
750 float cavernBottomHeight = cavernElevation;
751 float cavernTopHeight = cavernHeight + cavernElevation;
752
753 float topUV = (previewEye == PreviewEye.Left) ? 1.0f : 0.5f;
754 float bottomUV = (previewEye == PreviewEye.Left) ? 0.5f : 0.0f;
755
756 float deltaAngle = cavernAngle / (float)numPanels;
757
758 // Create vertices of surface.
759 for (int i = 0; i <= numPanels; i++)
760 {
761 float ratio = (float)i / (float)numPanels;
762 float currAngle = (ratio - 0.5f) * cavernAngle;
763
764 // Take note that angle 0 points down the Z-axis, not the X-axis.
765 float directionX = Mathf.Sin(currAngle * Mathf.Deg2Rad);
766 float directionZ = Mathf.Cos(currAngle * Mathf.Deg2Rad);
767
768 positions[i * 2] = new Vector3(cavernRadius * directionX, cavernTopHeight, cavernRadius * directionZ); // Top vertex.
769 normals[i * 2] = new Vector3(cavernRadius * directionX, 0.0f, cavernRadius * directionZ);
770 uvs[i * 2] = new Vector2((float)i / (float)numPanels, topUV);
771
772 positions[i * 2 + 1] = new Vector3(cavernRadius * directionX, cavernBottomHeight, cavernRadius * directionZ); // Top vertex.
773 normals[i * 2 + 1] = new Vector3(cavernRadius * directionX, 0.0f, cavernRadius * directionZ);
774 uvs[i * 2 + 1] = new Vector2((float)i / (float)numPanels, bottomUV);
775 }
776
777 // Assign indices of each panel.
778 // Each panel is a quad made up of 2 triangles.
779 // Unity uses a CLOCKWISE WINDING ORDER for its triangles.
780 for (int i = 0; i < numPanels; ++i)
781 {
782 // Triangle 1
783 indices[i * 6] = i * 2;
784 indices[i * 6 + 1] = i * 2 + 2;
785 indices[i * 6 + 2] = i * 2 + 1;
786
787 // Triangle 2
788 indices[i * 6 + 3] = i * 2 + 1;
789 indices[i * 6 + 4] = i * 2 + 2;
790 indices[i * 6 + 5] = i * 2 + 3;
791 }
792
793 mesh.name = "Cavern Mesh";
794 mesh.vertices = positions;
795 mesh.normals = normals;
796 mesh.uv = uvs;
797 mesh.triangles = indices;
798 return mesh;
799 }
800
801 /// \*brief
802 /// Generate a curved screen mesh.
803 /// \*warning Ensure that the mesh's material disables back-face culling!
804 private void CreatePreviewMesh()
805 {
806 previewMesh = GenerateMesh();
807 previewMesh.name = "Cavern Preview Mesh";
808
809 }
810
811 private void CreatePreviewTexture()
812 {
813 previewTexture = new RenderTexture((int)previewResolution, (int)previewResolution, 32, RenderTextureFormat.ARGB32);
814 previewTexture.dimension = TextureDimension.Tex2D;
815 previewTexture.wrapMode = TextureWrapMode.Clamp;
816 }
817
818 private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
819 {
820 // "Highjack" the GUI camera insert a render pass into the URP RenderGraph to render the output.
821 if (camera == guiCamera)
822 {
823 camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(cavernRenderPass);
824 }
825 }
826
827 private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) { }
828
829#if UNITY_EDITOR
830 private void OnValidate()
831 {
832 // This method is called whenever a setting is changed in the inspector, or at the beginning of scene mode rendering.
833 // If any of the Cavern size settings are changed, we need to regenerate the mesh.
834 CreatePreviewMesh();
835 settingsChanged.Invoke();
836 }
837
838 // The cubemap render targets get cleaned up by Unity's garbage collector on scene save or assembly reload. The material needs to have it's texture references restored.
839 private void OnSceneSaved(UnityEngine.SceneManagement.Scene scene)
840 {
841 CreateMaterial();
842 }
843
844 private void OnEditorDelayCall()
845 {
846 CreateMaterial();
847 }
848
849 private void OnDrawGizmos()
850 {
851 if (previewMaterial == null)
852 {
853 Debug.LogAssertion("CavernRenderer: Preview material cannot be null!");
854 }
855 previewMaterial.SetPass(0);
856 previewMaterial.mainTexture = livePreview ? previewTexture : null;
857
858 // We need to use Graphics.DrawMeshNow instead of Gizmos.DrawMesh so we can get a texture on it.
859 Graphics.DrawMeshNow(previewMesh, transform.position, transform.rotation);
860 }
861#endif
862 }
863}
void SetStereoscopicMode(StereoscopicMode mode)
CubemapResolution GetCubemapResolution()
StereoscopicMode GetStereoscopicMode()