Home > database >  How to properly change color of objects individually from Camera Ray-cast
How to properly change color of objects individually from Camera Ray-cast

Time:10-28

I have a drone player set up that has two cameras on it: a first person and a topdown camera, and i want to be able able to "scan" particular zones in the world by using the camera as a scanner. Currently i have a script set up that holds on the game objects to be scanned and handles the ray-casting and gradual color change of the objects. Currently, when i switch to first-person camera, all of my objects start turning red even though they each have different materials assigned to them. My current code is attached below. I only want to be able to turn one zone at a time to red while either the FPS or topdown cameras is looking directly at it.

`

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

public class ZoneController : MonoBehaviour
{
    private Camera frontViewCamera;
    private Camera topDownCamera;

    public Material[] zones;

    public float speed = 0.05f;

    private Color startColor, endColor = Color.red;

    // Start is called before the first frame update
    void Start()
    {
        frontViewCamera = GameObject.FindGameObjectWithTag("FrontFaceCam").GetComponent<Camera>() as Camera;
        topDownCamera = GameObject.FindGameObjectWithTag("TopDownCam").GetComponent<Camera>() as Camera;

        foreach (var zone in zones)
        {
            zone.color = Color.white;
        }

        startColor = Color.white;
    }


    private IEnumerator ChangeColour()
    {
        float tick = 0f;

        foreach (var zone in zones)
        {
            while (zone.color != endColor)
            {
                tick  = Time.deltaTime * speed;
                zone.color = Color.Lerp(startColor, endColor, tick);
                yield return null;
            }
        }
    }

    private void StartFadeToRed()
    {
        Ray frontCameraRayCast;
        Ray topDownCameraRayCast;
        RaycastHit fHit;
        RaycastHit tHit;

        frontCameraRayCast = frontViewCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
        topDownCameraRayCast = topDownCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));

        if (frontViewCamera.enabled)
        {
            if (Physics.Raycast(frontCameraRayCast, out fHit))
            {
                Debug.DrawRay(frontCameraRayCast.origin, frontCameraRayCast.direction * 10, Color.red);
                StartCoroutine("ChangeColour");
            }
        }


        if (topDownCamera.enabled)
        {
            if (Physics.Raycast(topDownCameraRayCast, out tHit) && tHit.collider.CompareTag("Zone"))
            {
                Debug.Log("Raycast topdwon");
                StartCoroutine("ChangeColour");
            }
        }
        
    }

    // Update is called once per frame
    void Update()
    {
        StartFadeToRed();
    }
}

`

CodePudding user response:

First, doing that color change on Update is horrible!!!! do it only once per camera switch.

Probably is because is getting stuck with so many coroutines due to the Update and float pointers never getting to a full Color.red, try doing once per object, abstract your code better.

I recommend you use DOTween, in a. few lines you can fix this.

CodePudding user response:

You are currently

  • potentially starting multiple concurrent Coroutines
  • Do exactly what you say you don't want to - fade all zone materials one after another
  • Which might potentially mean multiple objects use the same material as well

What you would rather want to do is

  • keep track of what you are hitting actually
  • get the material instance of according object(s)
  • start to fade those to red
  • also reset them when not being hit anymore

Now where and how you implement which part is up to you of course - I would probably have a Zone component on each of your zone objects like e.g.

public class Zone : MonoBehaviour
{
    // the renderers to fade to red - if only one you could simplify this of course
    [Header("References")]
    [SerializeField] private Renderer[] _renderersToFade;
    // How long should a full fading take?
    [Header("Settings")]
    [SerializeField] private float _fadeDuration = 1f;

    // is this zone currently being hit?
    private bool isHit;
    // current color interpolation factor
    private float _currentFactor;
    // currently running fade routine
    private Coroutine _currentRoutine;

    public bool IsHit
    {
        get => isHit;
        set
        {
            // value is the same as current -> do nothing
            if (value == isHit) return;
 
            // set the field
            isHit = value;

            // if was fading interrupt the current routine
            if (_currentRoutine != null)
            {
                StopCoroutine(_currentRoutine);
            }

            // start a new fade routine
            _currentRoutine = StartCoroutine(FadeRoutine());
        }
    }

    // keeps track of the original material colors per renderer
    private readonly Dictionary<Renderer, Color> _originalColors = new();

    private void Awake()
    { 
        // initialize the original color dictionary
        foreach (var renderer in _renderersToFade)
        {
            _originalColors.Add(renderer, renderer.material.color);
        }
    }

    private IEnumerator FadeRoutine()
    {
        // are we fading in or out?
        var targetFactor = isHit ? 1 : 0;

        // fade until reaching the target factor
        while (!Mathf.Approximately(_currentFactor, targetFactor))
        {
            // move the factor towards the target factor
            _currentFactor = Mathf.MoveTowards(_currentFactor, targetFactor, Time.deltaTime / _fadeDuration);

            // update all renderer colors
            foreach (var renderer in _renderersToFade)
            {
                renderer.material.color = Color.Lerp(_originalColors[renderer], Color.red, _currentFactor);
            }

            // go to next frame and continue
            yield return null;
        }
    }
}

And then control the via your central ZoneController where you want to store the current hit and also reset it once it is not hit anymore like e.g.

public class ZoneController : MonoBehaviour
{
    // if possible rather already reference these via the Inspector
    [SerializeField] private Camera frontViewCamera;
    [SerializeField] private Camera topDownCamera;

    // will keep reference to the currently hit zone
    private Zone _currentHitZone;

    private readonly Vector2 _viewportCenter = Vector2.one * 0.5f;

    private void Start()
    {
        // only as fallback get those on runtime
        if (!frontViewCamera) frontViewCamera = GameObject.FindGameObjectWithTag("FrontFaceCam").GetComponent<Camera>();
        if (!topDownCamera) topDownCamera = GameObject.FindGameObjectWithTag("TopDownCam").GetComponent<Camera>();
    }

    private void Update()
    {
        // Get the currently active camera
        Camera activeCamera;
        if (frontViewCamera.enabled) activeCamera = frontViewCamera;
        else if (topDownCamera.enabled) activeCamera = topDownCamera;
        else activeCamera = null;

        // no camera active at all? - should actually never happen ;)
        if (!activeCamera) return;

        // get the ray
        var ray = activeCamera.ViewportPointToRay(_viewportCenter);
        if (Physics.Raycast(ray, out var frontHit))
        {
            if (!frontHit.transform.TryGetComponent<Zone>(out var frontHitZone))
            {
                // Hitting something but it is not a Zone
                Debug.DrawLine(ray.origin, frontHit.point, Color.yellow);
                HitZone(null);
            }
            else
            {
                // Hitting a Zone
                Debug.DrawLine(ray.origin, frontHit.point, Color.red);
                HitZone(frontHitZone);
            }
        }
        else
        {
            // Hitting nothing at all
            Debug.DrawRay(ray.origin, ray.direction * 10, Color.green);
            HitZone(null);
        }
    }

    private void HitZone(Zone hitZone)
    {
        // hitting still the same zone? -> ignore
        if (_currentHitZone == hitZone) return;

        // did we hit something before? -> set it to not hit anymore
        if (_currentHitZone) _currentHitZone.IsHit = false;

        // store the new hit
        _currentHitZone = hitZone;

        // are currently hitting something? -> set to hit
        if (_currentHitZone) _currentHitZone.IsHit = true;
    }
}

enter image description here

  • Related