Top-Gear Seats Showcase Game

Last Updated On – 22/SEPT/2017
Development Duration – June 2017 to September 2017
Team – Digital Media team at Tagbin Services Pvt. Ltd.
My Role – Team Lead and Game Developer
Current Status – iOS and Android Beta Stage
Project Short Brief – Top-Gear Seats manufacturer in India needed a 3D game application via which they could showcase their products to customers and sales executives as they had around 1200 varieties of car seats and 2400 different colored or styled fabrics. This game was created fully in Unity3D Game Engine.
Technologies Used:
1. Unity 3D Game Engine
2. Adobe Photoshop
3. Visual Studio for C-Sharp Coding.
4. XCode for iOS Game Authoring.
5. Android Studio for Android Game Authoring.
6. Swift, Objective-C & Java language scripting for Camera and Scan-Tech plugin creation.
7. ARKit and Vuforia for Augmented reality features development.

TGS Prototype Game-play Functionality.

Scripts

1. Custom Camera Movement Controller

As the 3D showcase view could be visualized in world space or in augmented reality, a custom touch-based camera control system was created.

using UnityEngine;
using System.Collections;

/*===============================================================
Product:    Top Gear Seats
Developer:  Ankit Sethi
Company:    Tagbin Services Pvt. Ltd.
Copyright:  @ COPYRIGHT 2017-2018. ALL RIGHTS RESERVED - Tagbin Services Pvt. Ltd.
Date:       6/21/2017 4:26:14 PM
Date:       6/21/2017 4:26:14 PM
================================================================*/

namespace Tagbin.Game.TGS.Managers {
    public class CameraMovement : MonoBehaviour
    {
        public Transform target;
        public Vector3 targetOffset;
        public float distance = 5.0f;
        public float maxDistance = 20;
        public float minDistance = .6f;
        public float xSpeed = 200.0f;
        public float ySpeed = 200.0f;
        public int yMinLimit = -80;
        public int yMaxLimit = 80;
        public int zoomRate = 40;
        public float zoomDampening = 5.0f;

        private float xDeg = 0.0f;
        private float yDeg = 0.0f;
        private float currentDistance;
        private float desiredDistance;
        private Quaternion currentRotation;
        private Quaternion desiredRotation;
        private Quaternion rotation;
        private Vector3 position;

        void Start() { Init(); }
        void OnEnable() { Init(); }

        public void Init()
        {
            //If there is no target, create a temporary target at 'distance' from the cameras current viewpoint
            if (!target)
            {
                GameObject go = new GameObject("Cam Target");
                go.transform.position = transform.position + (transform.forward * distance);
                target = go.transform;
            }

            distance = Vector3.Distance(transform.position, target.position);
            currentDistance = distance;
            desiredDistance = distance;

            //be sure to grab the current rotations as starting points.
            position = transform.position;
            rotation = transform.rotation;
            currentRotation = transform.rotation;
            desiredRotation = transform.rotation;

            xDeg = Vector3.Angle(Vector3.right, transform.right);
            yDeg = Vector3.Angle(Vector3.up, transform.up);
        }

        /*
         * Camera logic on LateUpdate to only update after all character movement logic has been handled. 
         */
        void LateUpdate()
        {

            Rotate();
            Zoom();
            Move();
        }

        void Rotate()
        {        
            //if the screen is touching just one finger and it is moving on the screen perform rotation of the camera
            if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Moved)
            {
                float swipeSpeed = Input.touches[0].deltaPosition.magnitude / Input.touches[0].deltaTime;

                xDeg += Input.touches[0].deltaPosition.x * xSpeed * Time.deltaTime * swipeSpeed * 0.00001f;
                yDeg -= Input.touches[0].deltaPosition.y * ySpeed * Time.deltaTime * swipeSpeed * 0.00001f;

                ////////OrbitAngle

                //Clamp the vertical axis for the orbit
                yDeg = ClampAngle(yDeg, yMinLimit, yMaxLimit);
                // set camera rotation 
                desiredRotation = Quaternion.Euler(yDeg, xDeg, 0);
                currentRotation = transform.rotation;

                rotation = Quaternion.Lerp(currentRotation, desiredRotation, Time.deltaTime * zoomDampening);
                transform.rotation = rotation;
            }
            //stop rotation if you click on the screen
            else if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Began)
            {
                desiredRotation = transform.rotation;
            }

            //continue rotation even after releasing the finger fro the screen
            if (transform.rotation != desiredRotation)
            {
                rotation = Quaternion.Lerp(transform.rotation, desiredRotation, Time.deltaTime * zoomDampening);
                transform.rotation = rotation;
            }       
        }

        void Zoom()
        {
            if (Input.touchCount == 2)
            {
                // Store both touches.
                Touch touchZero = Input.GetTouch(0);
                Touch touchOne = Input.GetTouch(1);

                // Find the position in the previous frame of each touch.
                Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
                Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;

                // Find the magnitude of the vector (the distance) between the touches in each frame.
                float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
                float touchDeltaMag = (touchZero.position - touchOne.position).magnitude;

                // Find the difference in the distances between each frame.
                float deltaMagnitudeDiff = prevTouchDeltaMag - touchDeltaMag;

                // affect the desired Zoom distance if we pinch
                desiredDistance += deltaMagnitudeDiff * Time.deltaTime * zoomRate * Mathf.Abs(desiredDistance) * 0.001f;
                //clamp the zoom min/max
                desiredDistance = Mathf.Clamp(desiredDistance, minDistance, maxDistance);
                // For smoothing of the zoom, lerp distance
                currentDistance = Mathf.Lerp(currentDistance, desiredDistance, Time.deltaTime * zoomDampening);
            }
        }

        void Move()
        {
            // calculate position based on the new currentDistance 
            position = target.position - (rotation * Vector3.forward * currentDistance + targetOffset);
            transform.position = position;
        }

        private static float ClampAngle(float angle, float min, float max)
        {
            if (angle < -360)
                angle += 360;
            if (angle > 360)
                angle -= 360;
            return Mathf.Clamp(angle, min, max);
        }

    }
    }

2. Game Level Manager

As there were many different unity scenes and each had different level, a custom game level manager script was written.

using System;
using System.Collections;
using System.Collections.Generic;
using Tagbin.Game.TGS.Scriptables;
using Tagbin.Game.TGS.Utils;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

/*===============================================================
Product:    Top Gear Seats
Developer:  Ankit Sethi
Company:    Tagbin Services Pvt. Ltd.
Copyright:  @ COPYRIGHT 2017-2018. ALL RIGHTS RESERVED - Tagbin Services Pvt. Ltd.
Date:       6/23/2017 1:25:10 PM
Date:       6/23/2017 1:25:10 PM
================================================================*/

namespace Tagbin.Game.TGS.Managers {
	public class GameLevelManager : MonoBehaviour {

        #region Variables.
        public static GameLevelManager instance;

        public bool test = false;

        public Image tgsLogo;

        public GameObject loadingScreenPanel;
        public Slider progressSlider;
        public TMPro.TextMeshProUGUI progressTextMesh;

        private TimeSpan _spannedTime;
        #endregion

        #region Fields and Properties.
        public const int LEVEL_LOADER_SCENE_INDEX = 0;
        public const int SEAT_VIEWER_SCENE_INDEX = 1;
        public const int THREE_D_MAT_VIEWER_SCENE_INDEX = 2;
        public const int UPPER_MAT_VIEWER_SCENE_INDEX = 3;
        public const int IN_CAR_VIEWER_SCENE_INDEX = 4;
        public const int AR_WITh_MARKER_SCENE_INDEX = 5;
        public const int AR_WITHOUT_MARKER_SCENE_INDEX = 6;
        #endregion

        #region Unity Methods.
        private void Awake() {
            //DontDestroyOnLoad(gameObject);
            if(GameLevelManager.instance == null) {
                GameLevelManager.instance = this;
            } else {
                Debug.LogWarning("A previously awakened GameLevelManager MonoBehaviour exists!", gameObject);
            }
        }
        // Use this for initialization
        void Start () {
            
        }
		
		// Update is called once per frame
		void Update () {
            if(GameSettingsManager.instance.settings.isLevelLoaderInitialized) {
                StartCoroutine(UIEffectsUtility.FadeImage(tgsLogo, true));

                LoadLevelAtStart();
            }

            if(test)
            {
                test = false;
                DemoProduct();
            }
        }
		#endregion
		
		#region Events and Delegations
		#endregion
		
		#region Custom Methods
        public void LoadLevel(int sceneBuildIndex) {
            StartCoroutine(LoadLevelAsync(sceneBuildIndex));
        }

        public void LoadLevelAtStart() {
            GameSettingsManager.instance.settings.isLevelLoaderInitialized = false;
            switch (GameSettingsManager.instance.settings.gameProduct.productType) {
                case -1:
                    Debug.LogWarning("No Product Type Set From App Yet. Please check the Native to Unity Communication Pipeline Again!");
                    break;
                case 0:
                    LoadLevel(SEAT_VIEWER_SCENE_INDEX);
                    break;
                case 1:
                    LoadLevel(THREE_D_MAT_VIEWER_SCENE_INDEX);
                    break;
                case 2:
                    LoadLevel(UPPER_MAT_VIEWER_SCENE_INDEX);
                    break;
                default:
                    break;
            }

            GameSettingsManager.instance.settings.isLevelLoaderInitialized = false;
        }

        private IEnumerator LoadLevelAsync(int sceneBuildIndex) {
            DateTime startTime = DateTime.Now;

            AsyncOperation operation = SceneManager.LoadSceneAsync(sceneBuildIndex);

            loadingScreenPanel.gameObject.SetActive(true);

            while(!operation.isDone) {
                float progress = Mathf.Clamp01(operation.progress / 0.1f);
                //Debug.Log("[LevelManager] Progress: " + progress);
                progressSlider.value = progress;
                progressTextMesh.text = 100f * progress + "%";

                yield return null;
            }

            DateTime endTime = DateTime.Now;

            _spannedTime = endTime - startTime;

            Debug.Log("[LevelManager] Spanned Time: " + _spannedTime.ToString());

            yield return new WaitForSeconds(1f);

            loadingScreenPanel.gameObject.SetActive(false);
        }

        private void DemoProduct() {
            GameProduct gp = new GameProduct();
            gp.productType = 0;
            gp.productID = 1;
            gp.gameID = 1;
            gp.productName = "Test Seat Cover";
            gp.isFirstTime = false;

            string demoJson = JsonUtility.ToJson(gp);

            GameSettingsManager.instance.settings.JSONToGameProduct(demoJson);
            GameSettingsManager.instance.settings.isLevelLoaderInitialized = true;
        }

        public void SetGameProduct(string productJson) {
            GameSettingsManager.instance.settings.JSONToGameProduct(productJson);
            GameSettingsManager.instance.settings.isLevelLoaderInitialized = true;
        }
		#endregion
	}
}

3. In-Car View Manager

As a seat prefab was manipulated in a separate scene and then viewable inside a car in another scene, therefore an in-car view manager was written to dynamically place the update seats automatically in an empty car.

using System;
using System.Collections;
using System.Collections.Generic;
using Tagbin.Game.TGS.Seats;
using Tagbin.Game.TGS.UI;
using UnityEngine;
 
/*===============================================================
Product:    Top Gear Seats
Developer:  Ankit Sethi
Company:    Tagbin Services Pvt. Ltd.
Copyright:  @ COPYRIGHT 2017-2018. ALL RIGHTS RESERVED - Tagbin Services Pvt. Ltd.
Date:       6/22/2017 2:18:00 PM
Date:       6/22/2017 2:18:00 PM
================================================================*/

namespace Tagbin.Game.TGS.Managers {
    [Serializable]
    public class SeatPartColors {
        public Color baseColor;
        public Color topOneColor;
        public Color topTwoColor;
    }

    [Serializable]
    public class MatColors {
        public Color baseColor;
        public Color topOneColor;
        public Color topTwoColor;
    }

    [Serializable]
    public struct CarSeat {
        public SeatPrefab seatPrefab;
        public Vector3 parentPosition;
    }

    [Serializable]
    public class CarMat {
        public GameObject matObject;
        public Renderer baseRenderer;
        public Renderer topOneRenderer;
        public Renderer topTwoRenderer;
    }

	public class InCarViewManager : MonoBehaviour {

        #region Variables.
        public RadialLoadingBar carLoadingBar;
        public GameObject carSeatsParent;
        public SeatPartColors seatpartColors;
        public MatColors matColors;
        public List<CarSeat> carSeats;
        public List<CarMat> carMats;

        [SerializeField]
        private int _currentProductCounter = 1;
        private int _minSeatRange = 1;
        private int _maxSeatRange = 1;

        private bool _threadRunning;
        private string _dataPath;
        #endregion

        #region Fields and Properties.
        #endregion

        #region Unity Methods.
        // Use this for initialization
        void Start () {
            _dataPath = Application.dataPath;

            carLoadingBar.gameObject.SetActive(true);
            _threadRunning = true;

            SetScene();

			Debug.Log ("InCarViewManager LocalPosition-->" + transform.localPosition);
			Debug.Log ("InCarViewManager Position-->" + transform.position);
        }
		
		// Update is called once per frame
		void Update () {
            if(_currentProductCounter < _minSeatRange)
                _currentProductCounter = _minSeatRange;

            if(_currentProductCounter > _maxSeatRange)
                _currentProductCounter = _maxSeatRange;

            if(_threadRunning) {
                carLoadingBar.gameObject.SetActive(true);
            } else {
                carLoadingBar.gameObject.SetActive(false);
            }
        }
        #endregion

        #region Events and Delegations
        #endregion

        #region Custom Methods
        private void SetSeatColors() {
            for(int i = 0; i < GameSettingsManager.settingsSO.customizedFrontSeat.parts.Count; i++) {
                switch(GameSettingsManager.settingsSO.customizedFrontSeat.parts[i].partType) {
                    case SeatPartType.BASE_FRONT:
                        seatpartColors.baseColor = GameSettingsManager.settingsSO.customizedFrontSeat.parts[i].materialColor;
                        matColors.baseColor = seatpartColors.baseColor;
                        break;
                    case SeatPartType.TOP_FRONT_CENTER:
                        seatpartColors.topOneColor = GameSettingsManager.settingsSO.customizedFrontSeat.parts[i].materialColor;
                        matColors.topOneColor = seatpartColors.topOneColor;
                        break;
                    case SeatPartType.TOP_FRONT_SIDE:
                        seatpartColors.topTwoColor = GameSettingsManager.settingsSO.customizedFrontSeat.parts[i].materialColor;
                        matColors.topTwoColor = seatpartColors.topTwoColor;
                        break;
                    default:
                        break;
                }
            }
        }

        private void SetMaterialToSeat() {
            _threadRunning = true;
            string seatFolderName = "Materials/Seat_Materials/Seat" + _currentProductCounter.ToString("D" + 3);
            Material[] nextSeatMaterials = Resources.LoadAll<Material>(seatFolderName);
            Resources.UnloadUnusedAssets();

            String[] nextSeatMaterialsNames = new string[nextSeatMaterials.Length];
            for(int k = 0; k < nextSeatMaterials.Length; k++) {
                nextSeatMaterialsNames[k] = nextSeatMaterials[k].name;
            }

            for(int i = 0; i < carSeats.Count; i++) {
                if(_currentProductCounter <= carSeats[i].seatPrefab.range.max && _currentProductCounter >= carSeats[i].seatPrefab.range.min) {
                    carSeats[i].seatPrefab.instantiatedObject.SetActive(true);
                    for(int j = 0; j < carSeats[i].seatPrefab.parts.Count; j++) {
                        switch(carSeats[i].seatPrefab.parts[j].partType) {
                            case SeatPartType.BASE_FRONT:
                                string baseFrontMatName = "Base_Front_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(baseFrontMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.baseColor;
                                    }
                                }
                                break;
                            case SeatPartType.BASE_BACK:
                                string baseBackMatName = "Base_Back_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(baseBackMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.baseColor;
                                    }
                                }
                                break;
                            case SeatPartType.TOP_FRONT_CENTER:
                                string topFrontCenterMatName = "Top_Front_Center_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(topFrontCenterMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.topOneColor;
                                    }
                                }
                                break;
                            case SeatPartType.TOP_FRONT_SIDE:
                                string topFrontSideMatName = "Top_Front_Side_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(topFrontSideMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.topTwoColor;
                                    }
                                }
                                break;
                            case SeatPartType.TOP_BACK:
                                string topBackMatName = "Top_Back_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(topBackMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.topOneColor;
                                    }
                                }
                                break;
                            case SeatPartType.TOP_BACK_PARTS:
                                string topBackPartsMatName = "Top_Back_Parts_Tex";
                                for(int n = 0; n < nextSeatMaterialsNames.Length; n++) {
                                    if(nextSeatMaterialsNames[n].Contains(topBackPartsMatName)) {
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material = nextSeatMaterials[n];
                                        //carSeats[i].seatPrefab.parts[j].partRenderer.sharedMaterial.color = carSeats[i].seatPrefab.parts[j].materialColor;
                                        carSeats[i].seatPrefab.parts[j].partRenderer.material.color = seatpartColors.topTwoColor;
                                    }
                                }
                                break;
                            default:
                                break;
                        }
                    }

                    //Debug.Log("[GameSettingsManager] Current Seat's Material: " + GameSettingsManager.settingsSO.customizedFrontSeat.parts[0].partRenderer.material.name);
                    //Debug.Log("[GameSettingsManager] Current Seat Instantiated GameObject: " + GameSettingsManager.settingsSO.customizedFrontSeat.instantiatedObject.name);
                    
                } else {
                    carSeats[i].seatPrefab.instantiatedObject.SetActive(false);
                }

            }

            _threadRunning = false;
        }

        private void SetMaterialToMats() {
            _threadRunning = true;
            string matFolderName = "Materials/3DMat_Materials/3DMat" + _currentProductCounter.ToString("D" + 3);
            Material[] nextMatMaterials = Resources.LoadAll<Material>(matFolderName);
            Resources.UnloadUnusedAssets();

            String[] nextMatMaterialsNames = new string[nextMatMaterials.Length];
            for(int k = 0; k < nextMatMaterials.Length; k++) {
                nextMatMaterialsNames[k] = nextMatMaterials[k].name;
            }

            for(int i = 0; i < carMats.Count; i++) {
                    

                    //Debug.Log("[GameSettingsManager] Current Seat's Material: " + GameSettingsManager.settingsSO.customizedFrontSeat.parts[0].partRenderer.material.name);
                    //Debug.Log("[GameSettingsManager] Current Seat Instantiated GameObject: " + GameSettingsManager.settingsSO.customizedFrontSeat.instantiatedObject.name);

                

            }

            _threadRunning = false;
        }

        public void SetColorToMats() {
            for(int i = 0; i < carMats.Count; i++) {
                carMats[i].baseRenderer.material.color = matColors.baseColor;
                carMats[i].topOneRenderer.material.color = matColors.topOneColor;
                carMats[i].topTwoRenderer.material.color = matColors.topTwoColor;
            }
        }

        public void SetScene() {
            switch(GameSettingsManager.settingsSO.gameProduct.productType) {
                case (int)Scriptables.ProductType.NOT_SET_YET:
                    GameLevelManager.instance.LoadLevel(GameLevelManager.LEVEL_LOADER_SCENE_INDEX);
                    break;
                case (int)Scriptables.ProductType.SEAT_COVER:
                    InitializeCustomizedSeatPrefabs();

                    _currentProductCounter = GameSettingsManager.settingsSO.gameProduct.gameID;

                    SetSeatColors();

                    SetMaterialToSeat();

                    SetColorToMats();
                    break;
                case (int)Scriptables.ProductType.THREE_D_MAT:
                    InitializeDefaultSeatPrefab();

                    _currentProductCounter = GameSettingsManager.settingsSO.gameProduct.gameID;

                    SetColorToMats();

                    SetMaterialToMats();
                    break;
                case (int)Scriptables.ProductType.UPPER_MAT:
                    InitializeDefaultSeatPrefab();

                    _currentProductCounter = GameSettingsManager.settingsSO.gameProduct.gameID;

                    SetMaterialToMats();

                    SetColorToMats();
                    break;
                default:
                    break;
            }
        }

        private void InitializeDefaultSeatPrefab() {
            for(int i = 0; i < 3; i++) {
                SeatPrefab seatPrefab = carSeats[i].seatPrefab;
                GameObject obj = (GameObject)Instantiate(seatPrefab.prefab, carSeatsParent.transform);
                obj.transform.localPosition = carSeats[i].parentPosition;

                carSeats[i].seatPrefab.instantiatedObject = obj;

                obj.SetActive(true);

                var seatPartIdentifiers = seatPrefab.instantiatedObject.transform.GetComponentsInChildren<SeatPartIdentifier>();
                for(int j = 0; j < seatPartIdentifiers.Length; j++) {
                    seatPrefab.parts.Add(seatPartIdentifiers[j].part);
                }

                int min = seatPrefab.range.min;
                if(min < _minSeatRange)
                    _minSeatRange = min;

                int max = seatPrefab.range.max;
                if(max > _maxSeatRange)
                    _maxSeatRange = max;
            }
        }

        private void InitializeCustomizedSeatPrefabs() {
            for(int i = 0; i < carSeats.Count; i++) {
                SeatPrefab seatPrefab = carSeats[i].seatPrefab;
                GameObject obj = (GameObject)Instantiate(seatPrefab.prefab, carSeatsParent.transform);
                obj.transform.localPosition = carSeats[i].parentPosition;

                carSeats[i].seatPrefab.instantiatedObject = obj;

                if(GameSettingsManager.settingsSO.customizedFrontSeat.category == seatPrefab.category)
                    obj.SetActive(true);
                else
                    obj.SetActive(false);

                var seatPartIdentifiers = seatPrefab.instantiatedObject.transform.GetComponentsInChildren<SeatPartIdentifier>();
                for(int j = 0; j < seatPartIdentifiers.Length; j++) {
                    seatPrefab.parts.Add(seatPartIdentifiers[j].part);
                }

                int min = seatPrefab.range.min;
                if(min < _minSeatRange)
                    _minSeatRange = min;

                int max = seatPrefab.range.max;
                if(max > _maxSeatRange)
                    _maxSeatRange = max;
            }
        }

        public void OnBackClick()
        {
            switch (GameSettingsManager.settingsSO.gameProduct.productType)
            {
                case (int)Scriptables.ProductType.NOT_SET_YET:
                    GameLevelManager.instance.LoadLevel(GameLevelManager.LEVEL_LOADER_SCENE_INDEX);
                    break;
                case (int)Scriptables.ProductType.SEAT_COVER:
                    GameLevelManager.instance.LoadLevel(GameLevelManager.SEAT_VIEWER_SCENE_INDEX);
                    break;
                case (int)Scriptables.ProductType.THREE_D_MAT:
                    GameLevelManager.instance.LoadLevel(GameLevelManager.THREE_D_MAT_VIEWER_SCENE_INDEX);
                    break;
                case (int)Scriptables.ProductType.UPPER_MAT:
                    GameLevelManager.instance.LoadLevel(GameLevelManager.UPPER_MAT_VIEWER_SCENE_INDEX);
                    break;
                default:
                    break;
            }
        }
        #endregion
    }
}

3. Mat Structure Utility

As each seat config had various different meshes and each had shared materials, it was necessary to create a structure to control and manage the materials on each variations.

using System;
using System.Collections;
using System.Collections.Generic;
using Tagbin.Game.TGS.Managers;
using Tagbin.Game.TGS.UI;
using UnityEngine;
 
/*===============================================================
Product:    Top Gear Seats
Developer:  Ankit Sethi
Company:    Tagbin Services Pvt. Ltd.
Copyright:  @ COPYRIGHT 2017-2018. ALL RIGHTS RESERVED - Tagbin Services Pvt. Ltd.
Date:       6/23/2017 4:21:28 PM
Date:       6/23/2017 4:21:28 PM
================================================================*/

namespace Tagbin.Game.TGS.Mats {
    [Serializable]
    public enum MatType {
        THREED_MAT,
        UPPER_MAT
    }

    [Serializable]
    public enum MatPartType {
        MAT_BASE,
        MAT_BORDER,
        MAT_UPPER
    }

    [Serializable]
    public struct MatRange {
        public int min;
        public int max;
    }

    [Serializable]
    public class MatPart {
        [SerializeField]
        public GameObject partObject;
        [SerializeField]
        public MeshRenderer partRenderer;
        [SerializeField]
        public Color materialColor;
        [SerializeField]
        public MatPartType partType;
    }

    [Serializable]
    public class MatPrefab {
        [SerializeField]
        public GameObject prefab;
        [SerializeField]
        public MatType type;
        [SerializeField]
        public GameObject instantiatedObject;
        [SerializeField]
        public List<MatPart> parts;
        [SerializeField]
        public MatRange range;
    }

	public class MatStructure : MonoBehaviour {

        #region Variables.
        public RadialLoadingBar matLoadingBar;
        [SerializeField]
        public List<MatPrefab> matPrefabs;

        [SerializeField]
        private int _currentMatCounter = 1;
        private int _minMatRange = 1;
        private int _maxMatRange = 1;

        private bool _threadRunning;
        private string _dataPath;

        private MatPartType _selectedPartTpe = MatPartType.MAT_BASE;

        [System.Runtime.InteropServices.DllImport("__Internal")]
        extern static public void NativeiOSBack();
        #endregion

        #region Fields and Properties.
        #endregion

        #region Unity Methods.
        // Use this for initialization
        void Start () {
            _dataPath = Application.dataPath;

            matLoadingBar.gameObject.SetActive(true);
            _threadRunning = true;

            for(int i = 0; i < matPrefabs.Count; i++) {
                MatPrefab matPrefab = matPrefabs[i];
                matPrefab.instantiatedObject = (GameObject)Instantiate(matPrefab.prefab, this.transform);

                if(i == 0)
                    matPrefab.instantiatedObject.SetActive(false);
                else
                    matPrefab.instantiatedObject.SetActive(false);

                var matPartIdentifiers = matPrefab.instantiatedObject.transform.GetComponentsInChildren<MatPartIdentifier>();
                for(int j = 0; j < matPartIdentifiers.Length; j++) {
                    matPartIdentifiers[i].part.materialColor = Color.white;
                    matPrefab.parts.Add(matPartIdentifiers[j].part);
                }

                int min = matPrefab.range.min;
                if(min < _minMatRange)
                    _minMatRange = min;

                int max = matPrefab.range.max;
                if(max > _maxMatRange)
                    _maxMatRange = max;

                matPrefabs[i] = matPrefab;
            }

            GetMatForGameId(GameSettingsManager.instance.settings.gameProduct.gameID);
        }
		
		// Update is called once per frame
		void Update () {
            if(_currentMatCounter < _minMatRange)
                _currentMatCounter = _minMatRange;

            if(_currentMatCounter > _maxMatRange)
                _currentMatCounter = _maxMatRange;

            if(_threadRunning) {
                matLoadingBar.gameObject.SetActive(true);
            } else {
                matLoadingBar.gameObject.SetActive(false);
            }
        }
        #endregion

        #region Events and Delegations
        #endregion

        #region Custom Methods
        public void GetMatForProductId(string productId) {
            try {
                _currentMatCounter = int.Parse(productId);

                SetMaterialToMat();
            } catch(Exception e) {
                Debug.LogException(e);
            }
        }

        public void GetMatForGameId(int gameId) {
            try {
                _currentMatCounter = gameId;

                SetMaterialToMat();
            } catch (Exception e) {
                Debug.LogException(e);
            }
        }

        private void SetMaterialToMat() {
            _threadRunning = true;
            string matFolderName = "Materials/3DMat_Materials/3DMat_" + _currentMatCounter.ToString("D" + 3);
            Material[] nextMatMaterials = Resources.LoadAll<Material>(matFolderName);
            Resources.UnloadUnusedAssets();

            String[] nextMatMaterialsNames = new string[nextMatMaterials.Length];
            for(int k = 0; k < nextMatMaterials.Length; k++) {
                nextMatMaterialsNames[k] = nextMatMaterials[k].name;
            }

            for(int i = 0; i < matPrefabs.Count; i++) {
                if(_currentMatCounter <= matPrefabs[i].range.max && _currentMatCounter >= matPrefabs[i].range.min) {
                    matPrefabs[i].instantiatedObject.SetActive(true);
                    for(int j = 0; j < matPrefabs[i].parts.Count; j++) {
                        switch(matPrefabs[i].parts[j].partType) {
                            case MatPartType.MAT_BASE:
                                GameSettingsManager.settingsSO.lastMainScene = Scriptables.SceneType.THREE_D_MAT_VIEWER;
                                string baseMatName = "TGS_Car_Matt_Base";
                                for(int n = 0; n < nextMatMaterialsNames.Length; n++) {
                                    if(nextMatMaterialsNames[n].Contains(baseMatName)) {
                                        Debug.Log(matPrefabs[i].parts[j].partRenderer);
                                        matPrefabs[i].parts[j].partRenderer.material = nextMatMaterials[n];
                                        matPrefabs[i].parts[j].materialColor = nextMatMaterials[n].color;
                                    }
                                }
                                break;
                            case MatPartType.MAT_BORDER:
                                GameSettingsManager.settingsSO.lastMainScene = Scriptables.SceneType.THREE_D_MAT_VIEWER;
                                string borderMatName = "TGS_Bottemmatts_blinn";
                                for(int n = 0; n < nextMatMaterialsNames.Length; n++) {
                                    if(nextMatMaterialsNames[n].Contains(borderMatName)) {
                                        matPrefabs[i].parts[j].partRenderer.material = nextMatMaterials[n];
                                        matPrefabs[i].parts[j].materialColor = nextMatMaterials[n].color;
                                    }
                                }
                                break;
                            case MatPartType.MAT_UPPER:
                                GameSettingsManager.settingsSO.lastMainScene = Scriptables.SceneType.UPPER_MAT_VIEWER;
                                string upperMatName = "TGS_Car_Upper_Matt";
                                for(int n = 0; n < nextMatMaterialsNames.Length; n++) {
                                    if(nextMatMaterialsNames[n].Contains(upperMatName)) {
                                        matPrefabs[i].parts[j].partRenderer.material = nextMatMaterials[n];
                                        matPrefabs[i].parts[j].materialColor = nextMatMaterials[n].color;
                                    }
                                }
                                break;
                            default:
                                break;
                        }
                    }
                
                    GameSettingsManager.settingsSO.customizedMat = matPrefabs[i];

                    break;
                } else {
                    matPrefabs[i].instantiatedObject.SetActive(false);
                }

            }

            _threadRunning = false;
        }

        public void OnColorPickerValueChange(Color newColor) {
            for(int i = 0; i < matPrefabs.Count; i++) {
                if(_currentMatCounter <= matPrefabs[i].range.max && _currentMatCounter >= matPrefabs[i].range.min) {
                    for(int j = 0; j < matPrefabs[i].parts.Count; j++) {
                        if(matPrefabs[i].parts[j].partType == _selectedPartTpe) {
                            matPrefabs[i].parts[j].partRenderer.material.color = newColor;
                            matPrefabs[i].parts[j].materialColor = newColor;
                        }
                    }
                }
            }
        }

        public void ToggleGameObject(GameObject obj) {
            obj.SetActive(!obj.activeSelf);
        }

        public void OnPartTypeButtonClick(int partType) {
            _selectedPartTpe = (MatPartType)partType;
        }

        public void OnInCarViewClick() {
            /*for(int i = 0; i < seatPrefabs.Count; i++) {
                seatPrefabs[i].instantiatedObject.SetActive(false);
            }*/
            //SceneManager.LoadScene(1);
            GameLevelManager.instance.LoadLevel(GameLevelManager.IN_CAR_VIEWER_SCENE_INDEX);
        }

        public void OnBackToNativeApp()
        {
            NativeiOSBack();
        }
        #endregion
    }
}

Gyrometer-Based Game View Controller

For In-Car View the camera needed to be controlled via mobile device’s gyro-meter readings but be bound to the world arena with some pre-defined constraints.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Tagbin.Game.TGS.Utils
{
    /// <summary>
    /// Gyroscope controller that works with any device orientation.
    /// </summary>
    public class GyroController : MonoBehaviour
    {
        #region [Private fields]

        private bool gyroEnabled = true;
        private const float lowPassFilterFactor = 0.2f;

        private readonly Quaternion baseIdentity = Quaternion.Euler(90, 0, 0);
        private readonly Quaternion landscapeRight = Quaternion.Euler(0, 0, 90);
        private readonly Quaternion landscapeLeft = Quaternion.Euler(0, 0, -90);
        private readonly Quaternion upsideDown = Quaternion.Euler(0, 0, 180);

        private Quaternion cameraBase = Quaternion.identity;
        private Quaternion calibration = Quaternion.identity;
        private Quaternion baseOrientation = Quaternion.Euler(90, 0, 0);
        private Quaternion baseOrientationRotationFix = Quaternion.identity;



        private Quaternion referanceRotation = Quaternion.identity;
        public bool debug = true;

        #endregion

        #region [Unity events]

        protected void Start()
        {
            AttachGyro();
        }

        protected void Update()
        {
            if (!gyroEnabled)
                return;
            transform.rotation = Quaternion.Slerp(transform.rotation,
                                                  cameraBase * (ConvertRotation(referanceRotation * Input.gyro.attitude) * GetRotFix()), lowPassFilterFactor);
        }

        protected void OnGUI()
        {
            if (!debug)
                return;

            GUILayout.Label("Orientation: " + Screen.orientation);
            GUILayout.Label("Calibration: " + calibration);
            GUILayout.Label("Camera base: " + cameraBase);
            GUILayout.Label("input.gyro.attitude: " + Input.gyro.attitude);
            GUILayout.Label("transform.rotation: " + transform.rotation);

            if (GUILayout.Button("On/off gyro: " + Input.gyro.enabled, GUILayout.Height(100)))
            {
                Input.gyro.enabled = !Input.gyro.enabled;
            }

            if (GUILayout.Button("On/off gyro controller: " + gyroEnabled, GUILayout.Height(100)))
            {
                if (gyroEnabled)
                {
                    DetachGyro();
                }
                else
                {
                    AttachGyro();
                }
            }

            if (GUILayout.Button("Update gyro calibration (Horizontal only)", GUILayout.Height(80)))
            {
                UpdateCalibration(true);
            }


            if (GUILayout.Button("Reset base orientation", GUILayout.Height(80)))
            {
                ResetBaseOrientation();
            }

            if (GUILayout.Button("Reset camera rotation", GUILayout.Height(80)))
            {
                transform.rotation = Quaternion.identity;
            }

            if (GUILayout.Button("Update camera base rotation (Horizontal only)", GUILayout.Height(80)))
            {
                UpdateCameraBaseRotation(true);
            }

            if (GUILayout.Button("Close", GUILayout.Height(80)))
            {
                debug = false;
            }
        }

        #endregion

        #region [Public methods]

        /// <summary>
        /// Attaches gyro controller to the transform.
        /// </summary>
        public void AttachGyro()
        {
            Debug.Log("test");
            gyroEnabled = true;
            ResetBaseOrientation();
            UpdateCalibration(true);
            UpdateCameraBaseRotation(true);
            RecalculateReferenceRotation();
        }

        /// <summary>
        /// Detaches gyro controller from the transform
        /// </summary>
        private void DetachGyro()
        {
            gyroEnabled = false;
        }

        #endregion

        #region [Private methods]

        /// <summary>
        /// Update the gyro calibration.
        /// </summary>
        private void UpdateCalibration(bool onlyHorizontal)
        {
            if (onlyHorizontal)
            {
                var fw = (Input.gyro.attitude) * (-Vector3.forward);
                fw.z = 0;
                if (fw == Vector3.zero)
                {
                    calibration = Quaternion.identity;
                }
                else
                {
                    calibration = (Quaternion.FromToRotation(baseOrientationRotationFix * Vector3.up, fw));
                }
            }
            else
            {
                calibration = Input.gyro.attitude;
            }
        }

        /// <summary>
        /// Update the camera base rotation.
        /// </summary>
        /// <param name='onlyHorizontal'>
        /// Only y rotation.
        /// </param>
        private void UpdateCameraBaseRotation(bool onlyHorizontal)
        {
            if (onlyHorizontal)
            {
                var fw = transform.forward;
                fw.y = 0;
                if (fw == Vector3.zero)
                {
                    cameraBase = Quaternion.identity;
                }
                else
                {
                    cameraBase = Quaternion.FromToRotation(Vector3.forward, fw);
                }
            }
            else
            {
                cameraBase = transform.rotation;
            }
        }

        /// <summary>
        /// Converts the rotation from right handed to left handed.
        /// </summary>
        /// <returns>
        /// The result rotation.
        /// </returns>
        /// <param name='q'>
        /// The rotation to convert.
        /// </param>
        private static Quaternion ConvertRotation(Quaternion q)
        {

            return new Quaternion(q.x, q.y, -q.z, -q.w);
        }

        /// <summary>
        /// Gets the rot fix for different orientations.
        /// </summary>
        /// <returns>
        /// The rot fix.
        /// </returns>
        private Quaternion GetRotFix()
        {
#if UNITY_3_5
        if (Screen.orientation == ScreenOrientation.Portrait)
            return Quaternion.identity;
         
        if (Screen.orientation == ScreenOrientation.LandscapeLeft || Screen.orientation == ScreenOrientation.Landscape)
            return landscapeLeft;
         
        if (Screen.orientation == ScreenOrientation.LandscapeRight)
            return landscapeRight;
         
        if (Screen.orientation == ScreenOrientation.PortraitUpsideDown)
            return upsideDown;
        return Quaternion.identity;
#else
            return Quaternion.identity;
#endif
        }

        /// <summary>
        /// Recalculates reference system.
        /// </summary>
        private void ResetBaseOrientation()
        {
            baseOrientationRotationFix = GetRotFix();
            baseOrientation = baseOrientationRotationFix * baseIdentity;
        }

        /// <summary>
        /// Recalculates reference rotation.
        /// </summary>
        private void RecalculateReferenceRotation()
        {
            referanceRotation = Quaternion.Inverse(baseOrientation) * Quaternion.Inverse(calibration);
        }

        #endregion
    }
}