diff --git a/CHANGELOG.md b/CHANGELOG.md index d4103c1e1..c48bdbe3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Built with [Unity 2020.2](https://github.com/freezy/VisualPinball.Engine/pull/255). ### Added +- Automated camera clipping ([#304](https://github.com/freezy/VisualPinball.Engine/pull/304/files)). - DMD and segment display support ([Documentation](https://docs.visualpinball.org/creators-guide/manual/displays.html)). - Plugin: Mission Pinball Framework ([Documentation](https://docs.visualpinball.org/plugins/mpf/index.html)) - Gamelogic Engine: Support for hardware rules ([#293](https://github.com/freezy/VisualPinball.Engine/pull/293)). diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png b/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png new file mode 100644 index 000000000..6b0a9ee95 Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png differ diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png.meta new file mode 100644 index 000000000..f1508403e --- /dev/null +++ b/VisualPinball.Unity/Assets/Editor/Icons/small_orange/camera_clipping.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: dd801084fa3a35646b55f2503bc1d775 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/CameraControllerInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/CameraControllerInspector.cs index be71cc191..37b92cff1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/CameraControllerInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/CameraControllerInspector.cs @@ -134,6 +134,67 @@ public override void OnInspectorGUI() EditorGUILayout.Space(); EditorGUILayout.Separator(); EditorGUILayout.PropertyField(_cameraPresetsProp); + + EditorGUILayout.Space(); + EditorGUILayout.Separator(); + + //Editor Play Mode Camera Controls + EditorGUILayout.LabelField("Runtime Motion Controls", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Horizontal Speed", EditorStyles.boldLabel); + _cameraController.mouseSpeedH = EditorGUILayout.Slider("", _cameraController.mouseSpeedH, 0f, 5f); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Vertical Speed", EditorStyles.boldLabel); + _cameraController.mouseSpeedV = EditorGUILayout.Slider("", _cameraController.mouseSpeedV, 0f, 5f); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Translation Speed", EditorStyles.boldLabel); + _cameraController.mouseSpeedT = EditorGUILayout.Slider("", _cameraController.mouseSpeedT, 0f, 5f); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.Separator(); + + /* + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Distance Change Multiplier", EditorStyles.boldLabel); + _cameraController.mouseSpeedD = EditorGUILayout.Slider("", _cameraController.mouseSpeedD, 0f, 2f); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("FOV Change Multiplier", EditorStyles.boldLabel); + _cameraController.mouseSpeedZ = EditorGUILayout.Slider("", _cameraController.mouseSpeedZ, 0f, 2f); + EditorGUILayout.EndHorizontal(); + */ + + EditorGUILayout.Space(); + EditorGUILayout.Separator(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Invert Horizontal Axis", EditorStyles.boldLabel); + _cameraController.invertX = EditorGUILayout.Toggle(_cameraController.invertX); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Invert Vertical Axis", EditorStyles.boldLabel); + _cameraController.invertY = EditorGUILayout.Toggle(_cameraController.invertY); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.Separator(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Use Inertia", EditorStyles.boldLabel); + _cameraController.useInertia = EditorGUILayout.Toggle(_cameraController.useInertia); + EditorGUILayout.EndHorizontal(); + + + } private void ApplySetting() diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs new file mode 100644 index 000000000..f9a9eec46 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs @@ -0,0 +1,141 @@ +// Visual Pinball Engine +// Copyright (C) 2021 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace VisualPinball.Unity +{ + [ExecuteAlways] + public class CameraClipPlane : MonoBehaviour + { + + [NonSerialized] + private Camera _camera; + + //Stores the table bounds. + private Bounds _tableBounds; + private Vector3 _previousCameraPosition = Vector3.zero; + + private void OnEnable() + { + SetClipPlanes(0.001f, 9f); + } + + private void Awake() + { + RetrieveCameraComponent(); + } + + private void Update() + { + if(_camera == null) + { + RetrieveCameraComponent(); + } + + if(_camera != null) + { + if(_previousCameraPosition != _camera.transform.position) + { + UpdateClipPlanes(); + _previousCameraPosition = _camera.transform.position; + } + } + } + + /// + /// Updates the camera clipping planes based on the table bounds. + /// + private void UpdateClipPlanes() + { + + //Early out if no table is selected. + if(!TableSelector.Instance.HasSelectedTable) + { + return; + } + + //Early out if we don't have a camera to work on. + if(_camera == null) + { + return; + } + + //Get selected table reference. + var table = TableSelector.Instance.SelectedTable; + + if(Application.isPlaying) + { + _tableBounds = table._tableBounds; //When playing at runtime, get the stored table bounds value instead of calculating. + } + else + { + _tableBounds = table.GetTableBounds(); //When in editor, calculate the bounds in case things have changed. + } + + var trans = _camera.transform; + + var cameraPos = trans.position; // camera position. + var sphereExtent = _tableBounds.extents.magnitude; //sphere radius of the bounds. + float cameraToCenter = Vector3.Distance(cameraPos, _tableBounds.center); + var nearPlane = math.max(cameraPos.magnitude - sphereExtent, 0.001f); //Assign initial near plane used when camera is not in the sphere. + var farPlane = math.max(1f, nearPlane + sphereExtent * 2f); //initial far bounds + + if(cameraToCenter < sphereExtent) + { + nearPlane = 0.01f; //camera is in the bounds so drop the near plane to very low. + farPlane = math.max(0.01f, sphereExtent + Vector3.Distance(cameraPos, _tableBounds.center)); //set far plane to delta between camera and furthest bound. + + } + + SetClipPlanes(nearPlane, farPlane); + + } + + /// + /// Gets the currently active or attached camera component. + /// + private void RetrieveCameraComponent() + { + _camera = GetComponent(); //Get the camera component we are attached to if present. + + if(_camera == null) + { + _camera = UnityEngine.Camera.current; //Get the current active camera if not on the camera component. + } + + } + + /// + /// Sets the camera clipping planes based on manually derived values. + /// + /// The near clip plane value + /// The far clip plane value + /// False when no camera could be found. + private void SetClipPlanes(float near, float far) + { + if(_camera == null) + { + return; + } + + _camera.nearClipPlane = math.max(0.001f, near); + _camera.farClipPlane = math.max(0.01f, far); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs.meta new file mode 100644 index 000000000..71efeeea6 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraClipPlane.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 869da57aeeeeca34f879e2e9be2e23bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: dd801084fa3a35646b55f2503bc1d775, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraController.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraController.cs index 7627e3895..8da299903 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraController.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraController.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; +using UnityEngine.InputSystem; namespace VisualPinball.Unity { @@ -28,9 +29,33 @@ public class CameraController : MonoBehaviour public List cameraPresets; + public float mouseSpeedH = 1f; //Horizontal Mouse Speed + public float mouseSpeedV = 1f; //Vertical Mouse Speed + public float mouseSpeedT =1f; //Translation Speed + //public float mouseSpeedD = 1f; //Distance Increment Speed. Disabled Temporarily + //public float mouseSpeedZ = 1f; //FOV Change Speed Disabled Temporarily + public bool useInertia = true; //Enables inertia on orientation change + public bool invertX = false; //Inverts the horizontal movement + public bool invertY = false; //Inverts the vertical movement + [NonSerialized] public Camera Camera; + + //Private variables used for motion functions. + private Vector3 _tableCenter = Vector3.zero; //Stored table center for offset + private float _hAccum = 0f; //Accumulated horizontal value + private float _vAccum = 0f; //Accumulated vertical value + private Vector3 _tAccum = Vector3.zero; //Translation accumulation + private float _cDistance = 0f; //Current camera local Z distance + private float _hInertial = 0f; //Horizontal inertia value + private float _vInertial = 0f; //Vertical inertia value + private bool _inertia = false; //Current inertia state + private Vector2 _lastAngularVector; //Last vector for motion from camera angle shift. + private Vector2 _prevLmbPosition = Vector2.zero;//Previous LMB Position + private Vector2 _prevRmbPosition = Vector2.zero; //Previous RMB Position + private float _cFOV; //Current FOV + private void OnEnable() { Camera = GetComponentInChildren(); @@ -53,17 +78,32 @@ private void OnEnable() } } + private void Start() + { + //Initialize values from active settings + _hAccum = activeSetting.orbit; + _vAccum = activeSetting.angle; + _cDistance = activeSetting.distance; + _cFOV = activeSetting.fov; + } + + /// + /// Applies the current controller settings to the camera. + /// public void ApplySetting() { - if (!activeSetting) { + if(!activeSetting) + { return; } - if (!TableSelector.Instance.HasSelectedTable) { + if(!TableSelector.Instance.HasSelectedTable) + { return; } var table = TableSelector.Instance.SelectedTable; + var trans = Camera.transform; var altitude = trans.transform.parent; var azimuth = altitude.parent; @@ -72,21 +112,174 @@ public void ApplySetting() trans.localPosition = new Vector3(0, 0, -activeSetting.distance); altitude.localRotation = Quaternion.Euler(new Vector3(activeSetting.angle, 0, 0)); azimuth.localRotation = Quaternion.Euler(new Vector3(0, activeSetting.orbit, 0)); - offset.localPosition = table.GetTableCenter() + activeSetting.offset; + Camera.fieldOfView = activeSetting.fov; - var tb = table.GetTableBounds(); - var p = trans.position; - var nearPoint = tb.ClosestPoint(p); - var deltaN = Vector3.Magnitude(p - nearPoint); - var deltaF = math.max(Vector3.Distance(p, tb.max), Vector3.Distance(p, tb.min)); + if(Application.isPlaying) + { + _tableCenter = table._tableCenter; + } + else + { + _tableCenter = table.GetTableCenter(); + } + + offset.localPosition = _tableCenter + activeSetting.offset; + } + + public void FixedUpdate() + { + if(!Application.isPlaying) + { + return; + } + + if(Mouse.current != null) + { + Debug.Log("I has mouse"); + + var trans = Camera.transform; + var altitude = trans.transform.parent; + var azimuth = altitude.parent; + var offset = azimuth.parent; + + + //LMB Initialization + if(Mouse.current.leftButton.wasPressedThisFrame) + { + _prevLmbPosition = Mouse.current.position.ReadValueFromPreviousFrame(); + float deltaMagnitude = Vector2.Distance(Mouse.current.position.ReadValue(), _prevLmbPosition); + + //Assume that a large magnitude means they clicked on another part of the screen and give it a default value. + if(deltaMagnitude > 10) + { + _prevLmbPosition = Mouse.current.position.ReadValue(); + + } + + _cDistance = Camera.transform.localPosition.z; + } + + //LMB Hold Behavior: Azimuth and Elevation adjustment. + if(Mouse.current.leftButton.isPressed) + { + var curPos = Mouse.current.position.ReadValue(); + float h = curPos.x - _prevLmbPosition.x; + float v = curPos.y - _prevLmbPosition.y; + + _prevLmbPosition = curPos; + + if(invertX) h = -h; + if(invertY) v = -v; + + _hAccum = math.lerp(_hAccum, _hAccum + h, Time.deltaTime * (3f * mouseSpeedH)); + _vAccum = math.lerp(_vAccum, _vAccum - v, Time.deltaTime * (3f * mouseSpeedV)); + + azimuth.localRotation = Quaternion.Euler(new Vector3(0, _hAccum, 0)); + altitude.localRotation = Quaternion.Euler(new Vector3(_vAccum, 0, 0)); + + + _lastAngularVector = Mouse.current.position.ReadValue() - Mouse.current.position.ReadValueFromPreviousFrame(); + _lastAngularVector.Normalize(); + _hInertial = _lastAngularVector.x * mouseSpeedH; + _vInertial = _lastAngularVector.y * mouseSpeedV; + + } - //TODO: Replace this with proper frustum distances. - var nearPlane = math.max(0.001f, math.abs(deltaN * 0.9f)); - var farPlane = math.max(1f, math.abs(deltaF*1.1f)); + //Scroll Behavior: Distance to pivot adjustment + if(Mouse.current.scroll.IsActuated() && !Keyboard.current.ctrlKey.isPressed) + { + var scrollDist = Mouse.current.scroll.ReadValue(); + _cDistance += 0.002f * scrollDist.y - Mouse.current.scroll.ReadValueFromPreviousFrame().y; + Camera.transform.localPosition = new Vector3(0, 0, math.clamp(_cDistance, -8f, -0.1f)); + + } + + //LMB Release + if(Mouse.current.leftButton.wasReleasedThisFrame) + { + _inertia = useInertia; + } + + + //RMB Initialization + if(Mouse.current.rightButton.wasPressedThisFrame) + { + //tAccum = Vector3.zero; // offset.localPosition; + _prevRmbPosition = Mouse.current.position.ReadValue(); + + } + + //RMB Hold Behavior: Pivot translation adjustment + if(Mouse.current.rightButton.isPressed) + { + var curTPos = Mouse.current.position.ReadValue(); + float h = curTPos.x - _prevRmbPosition.x; + float v = curTPos.y - _prevRmbPosition.y; + + //Angle correction based on current azimuth + var theta = math.radians(-azimuth.localRotation.eulerAngles.y); + var cos = math.cos(theta); + var sin = math.sin(theta); + + if(invertX) h = -h; + if(invertY) v = -v; + + h = -h; + v = -v; + + var newh = h * cos - v * sin; + var newv = h * sin + v * cos; + + var posDelta = new Vector3(newh*0.03f, 0, newv*0.03f); + _prevRmbPosition = curTPos; + + _tAccum = Vector3.Lerp(_tAccum, _tAccum + posDelta, Time.deltaTime * (3f * mouseSpeedT)); + + + offset.localPosition = _tableCenter + _tAccum; + + } + + //FOV Control + if(Mouse.current.scroll.IsActuated() && Keyboard.current.ctrlKey.isPressed) + { + var scrollDist = Mouse.current.scroll.ReadValue(); + _cFOV += 0.002f * scrollDist.y - Mouse.current.scroll.ReadValueFromPreviousFrame().y; + Camera.fieldOfView = _cFOV; + + } + + //Inertia + if(_inertia == true) + { + _hInertial *= 0.8f; + _vInertial *= 0.8f; + + _hAccum += _hInertial * mouseSpeedH; + _vAccum += -_vInertial * mouseSpeedV; + + azimuth.localRotation = Quaternion.Euler(new Vector3(0, _hAccum, 0)); + altitude.localRotation = Quaternion.Euler(new Vector3(_vAccum, 0, 0)); + + + if(_hInertial <= 0.01f && _vInertial <= 0.01f) + { + _inertia = false; + } + } + } - Camera.nearClipPlane = nearPlane; - Camera.farClipPlane = farPlane; } + + public void AdjustCameraHorizontal(float amount) + { + activeSetting.orbit += amount; + ApplySetting(); + } + + + + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs index 3818b23a1..87bbeb7b1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs @@ -85,8 +85,20 @@ public class TableAuthoring : ItemMainRenderableAuthoring private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + //Private runtime values needed for camera adjustments. + [HideInInspector] [SerializeField] public Bounds _tableBounds; + [HideInInspector] [SerializeField] public Vector3 _tableCenter; + + public void Awake() + { + //Store table information + _tableBounds = GetTableBounds(); + _tableCenter = GetTableCenter(); + } + protected virtual void Start() { + if (EngineProvider.Exists) { EngineProvider.Get().Init(this); } @@ -266,13 +278,15 @@ public Vector3 GetTableCenter() public Bounds GetTableBounds() { + var tableBounds = new Bounds(); + var mrs = GetComponentsInChildren(); - foreach(var mr in mrs) { - tableBounds.Encapsulate(mr.bounds.max); - tableBounds.Encapsulate(mr.bounds.min); - tableBounds.Encapsulate(mr.bounds.center); + foreach(var mr in mrs) + { + tableBounds.Encapsulate(mr.bounds); } + return tableBounds; }