Dans Unity, les objets avec un composant Renderer peuvent utiliser les événements OnBecameVisible et OnBecameInvisible pour la détection de visibilité. Cepednant, pour les objets sans Renderer, il est nécessaire de gérer manuellement la détection d'intersection avec le frustum de la caméra. L'algorithme présenté ici offre une efficacité environ deux fois supérieure à GeometryUtility de Unity pour la détection de cylindres, sans allocation mémoire (garbage-free). Il est spécifiquement conçu pour les cylindres.
Voici une implémentation en C# adaptée à partir d'une version originale en C++. La classe principale a été restructurée pour améliorer la lisibilité et modifier la logique de calcul tout en conservant la précision mathématique.
using UnityEngine;
public class FrustumClipper
{
private float _distPlanProche;
private float _distPlanLointain;
private float _coeffXGaucheDroite;
private float _coeffZGaucheDroite;
private float _coeffYHautBas;
private float _coeffZHautBas;
public FrustumClipper(float focalLength, float aspectRatio, float nearPlane, float farPlane)
{
_distPlanProche = nearPlane;
_distPlanLointain = farPlane;
float invNorm = 1.0f / Mathf.Sqrt(1.0f + 1.0f);
_coeffXGaucheDroite = focalLength * invNorm;
_coeffZGaucheDroite = invNorm;
invNorm = 1.0f / Mathf.Sqrt(focalLength * focalLength + aspectRatio * aspectRatio);
_coeffYHautBas = focalLength * invNorm;
_coeffZHautBas = aspectRatio * invNorm;
}
public bool TestCylinderVisibility(Vector3 pointA, Vector3 pointB, float radius)
{
Vector3 axeCylindre = (pointB - pointA).normalized;
// Test du plan proche (normal = (0, 0, -1))
float projA = -pointA.z;
float projB = -pointB.z;
float rayonEffectif = radius * Mathf.Sqrt(1.0f - axeCylindre.z * axeCylindre.z);
float seuilProche = _distPlanProche - rayonEffectif;
bool dansProcheA = projA > seuilProche;
bool dansProcheB = projB > seuilProche;
if (!dansProcheA)
{
if (!dansProcheB) return false;
float t = (seuilProche + pointA.z) / axeCylindre.z;
pointA = new Vector3(
pointA.x - t * axeCylindre.x,
pointA.y - t * axeCylindre.y,
-seuilProche
);
}
else if (!dansProcheB)
{
float t = (seuilProche + pointA.z) / axeCylindre.z;
pointB = new Vector3(
pointA.x - t * axeCylindre.x,
pointA.y - t * axeCylindre.y,
-seuilProche
);
}
// Test du plan lointain (normal = (0, 0, 1))
float seuilLointain = _distPlanLointain + rayonEffectif;
dansProcheA = projA < seuilLointain;
dansProcheB = projB < seuilLointain;
if (!dansProcheA)
{
if (!dansProcheB) return false;
float t = (seuilLointain + pointA.z) / (pointB.z - pointA.z);
pointA = new Vector3(
pointA.x - t * (pointB.x - pointA.x),
pointA.y - t * (pointB.y - pointA.y),
-seuilLointain
);
}
else if (!dansProcheB)
{
float t = (seuilLointain + pointA.z) / (pointB.z - pointA.z);
pointB = new Vector3(
pointA.x - t * (pointB.x - pointA.x),
pointA.y - t * (pointB.y - pointA.y),
-seuilLointain
);
}
// Test du plan gauche
float nx = _coeffXGaucheDroite;
float nz = _coeffZGaucheDroite;
float dotA = nx * pointA.x - nz * pointA.z;
float dotB = nx * pointB.x - nz * pointB.z;
float projDir = nx * axeCylindre.x - nz * axeCylindre.z;
rayonEffectif = -radius * Mathf.Sqrt(1.0f - projDir * projDir);
bool dansGaucheA = dotA > rayonEffectif;
bool dansGaucheB = dotB > rayonEffectif;
if (!dansGaucheA)
{
if (!dansGaucheB) return false;
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointA = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
else if (!dansGaucheB)
{
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointB = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
// Test du plan droit
dotA = -nx * pointA.x - nz * pointA.z;
dotB = -nx * pointB.x - nz * pointB.z;
projDir = -nx * axeCylindre.x - nz * axeCylindre.z;
rayonEffectif = -radius * Mathf.Sqrt(1.0f - projDir * projDir);
bool dansDroitA = dotA > rayonEffectif;
bool dansDroitB = dotB > rayonEffectif;
if (!dansDroitA)
{
if (!dansDroitB) return false;
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointA = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
else if (!dansDroitB)
{
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointB = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
// Test du plan haut
float ny = _coeffYHautBas;
nz = _coeffZHautBas;
dotA = -ny * pointA.y - nz * pointA.z;
dotB = -ny * pointB.y - nz * pointB.z;
projDir = -ny * axeCylindre.y - nz * axeCylindre.z;
rayonEffectif = -radius * Mathf.Sqrt(1.0f - projDir * projDir);
bool dansHautA = dotA > rayonEffectif;
bool dansHautB = dotB > rayonEffectif;
if (!dansHautA)
{
if (!dansHautB) return false;
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointA = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
else if (!dansHautB)
{
float t = (rayonEffectif - dotA) / (dotB - dotA);
pointB = new Vector3(
pointA.x + t * (pointB.x - pointA.x),
pointA.y + t * (pointB.y - pointA.y),
pointA.z + t * (pointB.z - pointA.z)
);
}
// Test du plan bas
dotA = ny * pointA.y - nz * pointA.z;
dotB = ny * pointB.y - nz * pointB.z;
projDir = ny * axeCylindre.y - nz * axeCylindre.z;
rayonEffectif = -radius * Mathf.Sqrt(1.0f - projDir * projDir);
bool dansBasA = dotA > rayonEffectif;
bool dansBasB = dotB > rayonEffectif;
return dansBasA || dansBasB;
}
}
Exemple de script de test pour évaluer les performances et visualiser le cylindre dans la scène Unity :
using UnityEngine;
public class TestFrustumClipper : MonoBehaviour
{
public float rayonCylindre = 1.0f;
public float hauteurCylindre = 1.0f;
private FrustumClipper _clipper;
private bool _estVisible;
void Awake()
{
float ratioAspect = (float)Screen.height / Screen.width;
_clipper = new FrustumClipper(
Camera.main.fieldOfView * Mathf.Deg2Rad,
ratioAspect,
Camera.main.nearClipPlane,
Camera.main.farClipPlane
);
}
void Update()
{
UnityEngine.Profiling.Profiler.BeginSample("Frustum Clipper Test");
Matrix4x4 matriceVersLocal = Camera.main.transform.worldToLocalMatrix;
Vector3 milieuHaut = transform.position + Vector3.up * hauteurCylindre * 0.5f;
Vector3 milieuBas = transform.position - Vector3.up * hauteurCylindre * 0.5f;
Vector3 pointLocalA = -matriceVersLocal.MultiplyPoint3x4(milieuHaut);
Vector3 pointLocalB = -matriceVersLocal.MultiplyPoint3x4(milieuBas);
_estVisible = _clipper.TestCylinderVisibility(pointLocalA, pointLocalB, rayonCylindre);
UnityEngine.Profiling.Profiler.EndSample();
}
void OnDrawGizmos()
{
Gizmos.color = _estVisible ? Color.red : Color.white;
DessinerCylindre(transform.position, rayonCylindre, hauteurCylindre);
}
private void DessinerCylindre(Vector3 centre, float rayon, float hauteur)
{
const int SEGMENTS = 16;
Vector3 haut = centre + hauteur * 0.5f * Vector3.up;
Vector3 bas = centre - hauteur * 0.5f * Vector3.up;
float anglePas = 360f / SEGMENTS;
for (int i = 0; i < SEGMENTS; i++)
{
Quaternion rotation = Quaternion.AngleAxis(anglePas * i, Vector3.up);
Vector3 direction = rotation * Vector3.forward * rayon;
Vector3 pointHaut = haut + direction;
Vector3 pointBas = bas + direction;
Gizmos.DrawLine(haut, pointHaut);
Gizmos.DrawLine(bas, pointBas);
Gizmos.DrawLine(pointHaut, pointBas);
}
}
}