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 westMask |= frontMask | (enableConvergence ? rightMask : 0); // Left Eye
483 eastMask |= frontMask | (enableConvergence ? leftMask : 0); // Right Eye
484
485 /******************* Looking South *******************/
486 if (Vector3.Angle(headOffset + southWestBoundary, Vector3.forward) < cavernAngle * 0.5f ||
487 Vector3.Angle(headOffset + southEastBoundary, Vector3.forward) < cavernAngle * 0.5f)
488 {
489 monoMask |= backMask;
490 eastMask |= backMask; // Left Eye, no need to account for convergence because that is already handled in 'Looking North'.
491 westMask |= backMask; // Right Eye, no need to account for convergence because that is already handled in 'Looking North'.
492 }
493
494 /******************* Looking East *******************/
495 if (Vector3.Angle(headOffset + northEastBoundary, Vector3.forward) < cavernAngle * 0.5f ||
496 Vector3.Angle(headOffset + southEastBoundary, Vector3.forward) < cavernAngle * 0.5f)
497 {
498 monoMask |= rightMask;
499 northMask |= rightMask | (enableConvergence ? backMask : 0); // Left Eye
500 southMask |= rightMask | (enableConvergence ? frontMask : 0); // Right Eye
501 }
502
503 /******************* Looking West *******************/
504 if (Vector3.Angle(headOffset + northWestBoundary, Vector3.forward) < cavernAngle * 0.5f ||
505 Vector3.Angle(headOffset + southWestBoundary, Vector3.forward) < cavernAngle * 0.5f)
506 {
507 monoMask |= leftMask;
508 southMask |= leftMask | (enableConvergence ? frontMask : 0); // Left Eye
509 northMask |= leftMask | (enableConvergence ? backMask : 0); // Right Eye
510 }
511
512 /******************* Top & Bottom Faces *******************/
513 if (Mathf.Abs(northEastBoundary.z) < Mathf.Abs(screenTop) || // Looking North
514 Mathf.Abs(northWestBoundary.z) < Mathf.Abs(screenTop) || // Looking North
515 Mathf.Abs(southEastBoundary.z) < Mathf.Abs(screenTop) || // Looking South
516 Mathf.Abs(southWestBoundary.z) < Mathf.Abs(screenTop))
517 { // Looking South
518 monoMask |= topMask;
519 eastMask |= topMask;
520 westMask |= topMask;
521 }
522 if (Mathf.Abs(northEastBoundary.z) < Mathf.Abs(screenBottom) || // Looking North
523 Mathf.Abs(northWestBoundary.z) < Mathf.Abs(screenBottom) || // Looking North
524 Mathf.Abs(southEastBoundary.z) < Mathf.Abs(screenBottom) || // Looking South
525 Mathf.Abs(southWestBoundary.z) < Mathf.Abs(screenBottom))
526 { // Looking South
527 monoMask |= bottomMask;
528 eastMask |= bottomMask;
529 westMask |= bottomMask;
530 }
531 if (Mathf.Abs(northEastBoundary.x) < Mathf.Abs(screenTop) || // Looking East
532 Mathf.Abs(southEastBoundary.x) < Mathf.Abs(screenTop) || // Looking East
533 Mathf.Abs(northWestBoundary.x) < Mathf.Abs(screenTop) || // Looking West
534 Mathf.Abs(southWestBoundary.x) < Mathf.Abs(screenTop))
535 { // Looking West
536 monoMask |= topMask;
537 northMask |= topMask;
538 southMask |= topMask;
539 }
540 if (Mathf.Abs(northEastBoundary.x) < Mathf.Abs(screenBottom) || // Looking East
541 Mathf.Abs(southEastBoundary.x) < Mathf.Abs(screenBottom) || // Looking East
542 Mathf.Abs(northWestBoundary.x) < Mathf.Abs(screenBottom) || // Looking West
543 Mathf.Abs(southWestBoundary.x) < Mathf.Abs(screenBottom))
544 { // Looking West
545 monoMask |= bottomMask;
546 northMask |= bottomMask;
547 southMask |= bottomMask;
548 }
549 }
550
551 private void RenderEyes()
552 {
553 // Use Camera.MonoOrStereoscopicEye.Left or Camera.MonoOrStereoscopicEye.Right to ensure that the cubemap follows the camera's rotation.
554 // Camera.MonoOrStereoscopicEye.Mono renders the cubemap to be aligned to the world's axes instead.
555 int monoMask = 0; int northMask = 0; int southMask = 0; int eastMask = 0; int westMask = 0;
556 GetRenderFaces(out monoMask, out northMask, out southMask, out eastMask, out westMask);
557 switch (stereoMode)
558 {
559 case StereoscopicMode.Mono:
560 eye.stereoSeparation = 0.0f;
561 eye.transform.rotation = gameObject.transform.rotation; // Set eye's global orientation to the screen's orientation, regardless of the head's orientation.
562 eye.transform.localPosition = Vector3.zero;
563 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.North], monoMask, Camera.MonoOrStereoscopicEye.Left);
564 break;
565 case StereoscopicMode.Stereo:
566 eye.stereoSeparation = 0.0f;
567 eye.transform.rotation = gameObject.transform.rotation; // Set eye's global orientation to the screen's orientation, regardless of the head's orientation.
568 eye.transform.localPosition = new Vector3(0.0f, 0.0f, interpupillaryDistance * 0.5f);
569 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.North], northMask, Camera.MonoOrStereoscopicEye.Left);
570 eye.transform.localPosition = new Vector3(0.0f, 0.0f, interpupillaryDistance * -0.5f);
571 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.South], southMask, Camera.MonoOrStereoscopicEye.Right);
572 eye.transform.localPosition = new Vector3(interpupillaryDistance * 0.5f, 0.0f, 0.0f);
573 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.East], eastMask, Camera.MonoOrStereoscopicEye.Right);
574 eye.transform.localPosition = new Vector3(interpupillaryDistance * -0.5f, 0.0f, 0.0f);
575 eye.RenderToCubemap(cubemaps[(int)CubemapIndex.West], westMask, Camera.MonoOrStereoscopicEye.Left);
576 eye.transform.localPosition = Vector3.zero;
577 break;
578 }
579
580 // Cavern Dimensions Uniforms
581 material.SetFloat("_CavernHeight", cavernHeight);
582 material.SetFloat("_CavernRadius", cavernRadius);
583 material.SetFloat("_CavernAngle", cavernAngle);
584 material.SetFloat("_CavernElevation", cavernElevation);
585
586 // Head Tracking Uniforms
587 material.SetVector("_HeadPosition", head.transform.localPosition);
588
589 // Stereoscopic Rendering Uniforms
590 material.SetInteger("_EnableStereoscopic", stereoMode == StereoscopicMode.Stereo ? 1 : 0);
591 material.SetInteger("_EnableConvergence", enableConvergence ? 1 : 0);
592 material.SetFloat("_InterpupillaryDistance", interpupillaryDistance);
593 material.SetInteger("_SwapEyes", swapEyes ? 1 : 0);
594 }
595
596 private void CreateCubemaps()
597 {
598 cubemaps = new RenderTexture[(int)CubemapIndex.Num];
599 for (int i = 0; i < (int)CubemapIndex.Num; ++i)
600 {
601 cubemaps[i] = new RenderTexture((int)cubemapResolution, (int)cubemapResolution, 32, RenderTextureFormat.ARGB32);
602 cubemaps[i].dimension = TextureDimension.Cube;
603 cubemaps[i].wrapMode = TextureWrapMode.Clamp;
604 }
605 }
606
607 private void CreateMaterial()
608 {
609 material = new Material(shader);
610 material.SetTexture("_CubemapNorth", cubemaps[(int)CubemapIndex.North]);
611 material.SetTexture("_CubemapSouth", cubemaps[(int)CubemapIndex.South]);
612 material.SetTexture("_CubemapEast", cubemaps[(int)CubemapIndex.East]);
613 material.SetTexture("_CubemapWest", cubemaps[(int)CubemapIndex.West]);
614 cavernRenderPass?.SetMaterial(material);
615 }
616
617 // generates a mesh used for the preview or other uses
618 public Mesh GenerateMesh()
619 {
620 Mesh mesh = new Mesh();
621 // Have about one panel every 10 degrees. A reasonable number.
622 int numPanels = Mathf.Max(1, (int)(cavernAngle / 10.0f));
623 int numVertices = (numPanels + 1) * 2;
624
625 Vector3[] positions = new Vector3[numVertices];
626 Vector3[] normals = new Vector3[numVertices];
627 Vector2[] uvs = new Vector2[numVertices];
628 int[] indices = new int[numPanels * 6];
629
630 /********************************************** Generate inner surface. **********************************************/
631
632 float cavernBottomHeight = cavernElevation;
633 float cavernTopHeight = cavernHeight + cavernElevation;
634
635 float topUV = (previewEye == PreviewEye.Left) ? 1.0f : 0.5f;
636 float bottomUV = (previewEye == PreviewEye.Left) ? 0.5f : 0.0f;
637
638 float deltaAngle = cavernAngle / (float)numPanels;
639
640 // Create vertices of surface.
641 for (int i = 0; i <= numPanels; i++)
642 {
643 float ratio = (float)i / (float)numPanels;
644 float currAngle = (ratio - 0.5f) * cavernAngle;
645
646 // Take note that angle 0 points down the Z-axis, not the X-axis.
647 float directionX = Mathf.Sin(currAngle * Mathf.Deg2Rad);
648 float directionZ = Mathf.Cos(currAngle * Mathf.Deg2Rad);
649
650 positions[i * 2] = new Vector3(cavernRadius * directionX, cavernTopHeight, cavernRadius * directionZ); // Top vertex.
651 normals[i * 2] = new Vector3(cavernRadius * directionX, 0.0f, cavernRadius * directionZ);
652 uvs[i * 2] = new Vector2((float)i / (float)numPanels, topUV);
653
654 positions[i * 2 + 1] = new Vector3(cavernRadius * directionX, cavernBottomHeight, cavernRadius * directionZ); // Top vertex.
655 normals[i * 2 + 1] = new Vector3(cavernRadius * directionX, 0.0f, cavernRadius * directionZ);
656 uvs[i * 2 + 1] = new Vector2((float)i / (float)numPanels, bottomUV);
657 }
658
659 // Assign indices of each panel.
660 // Each panel is a quad made up of 2 triangles.
661 // Unity uses a CLOCKWISE WINDING ORDER for its triangles.
662 for (int i = 0; i < numPanels; ++i)
663 {
664 // Triangle 1
665 indices[i * 6] = i * 2;
666 indices[i * 6 + 1] = i * 2 + 2;
667 indices[i * 6 + 2] = i * 2 + 1;
668
669 // Triangle 2
670 indices[i * 6 + 3] = i * 2 + 1;
671 indices[i * 6 + 4] = i * 2 + 2;
672 indices[i * 6 + 5] = i * 2 + 3;
673 }
674
675 mesh.name = "Cavern Mesh";
676 mesh.vertices = positions;
677 mesh.normals = normals;
678 mesh.uv = uvs;
679 mesh.triangles = indices;
680 return mesh;
681 }
682
683 /// \*brief
684 /// Generate a curved screen mesh.
685 /// \*warning Ensure that the mesh's material disables back-face culling!
686 private void CreatePreviewMesh()
687 {
688 previewMesh = GenerateMesh();
689 previewMesh.name = "Cavern Preview Mesh";
690
691 }
692
693 private void CreatePreviewTexture()
694 {
695 previewTexture = new RenderTexture((int)previewResolution, (int)previewResolution, 32, RenderTextureFormat.ARGB32);
696 previewTexture.dimension = TextureDimension.Tex2D;
697 previewTexture.wrapMode = TextureWrapMode.Clamp;
698 }
699
700 private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
701 {
702 // "Highjack" the GUI camera insert a render pass into the URP RenderGraph to render the output.
703 if (camera == guiCamera)
704 {
705 camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(cavernRenderPass);
706 }
707 }
708
709 private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) { }
710
711#if UNITY_EDITOR
712 private void OnValidate()
713 {
714 // This method is called whenever a setting is changed in the inspector, or at the beginning of scene mode rendering.
715 // If any of the Cavern size settings are changed, we need to regenerate the mesh.
716 CreatePreviewMesh();
717 settingsChanged.Invoke();
718 }
719
720 // 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.
721 private void OnSceneSaved(UnityEngine.SceneManagement.Scene scene)
722 {
723 CreateMaterial();
724 }
725
726 private void OnEditorDelayCall()
727 {
728 CreateMaterial();
729 }
730
731 private void OnDrawGizmos()
732 {
733 if (previewMaterial == null)
734 {
735 Debug.LogAssertion("CavernRenderer: Preview material cannot be null!");
736 }
737 previewMaterial.SetPass(0);
738 previewMaterial.mainTexture = livePreview ? previewTexture : null;
739
740 // We need to use Graphics.DrawMeshNow instead of Gizmos.DrawMesh so we can get a texture on it.
741 Graphics.DrawMeshNow(previewMesh, transform.position, transform.rotation);
742 }
743#endif
744 }
745}
void SetStereoscopicMode(StereoscopicMode mode)
CubemapResolution GetCubemapResolution()
StereoscopicMode GetStereoscopicMode()