[C#] Transformer LOD par tailles (standard) en LOD par distances

Cette section est destinée aux scripts partagés par la communauté. Chaque post est destiné à un script. Suivez bien les recommandations.
Ran
Messages : 21
Inscription : 04 Déc 2015 10:43

[C#] Transformer LOD par tailles (standard) en LOD par distances

Message par Ran » 14 Nov 2017 10:40

Unity 2017 semble ne gérer ses niveaux de détail (LOD) automatiques que par pourcentage de place occupée à l'écran. Cela signifie que le changement de niveau de détail ne se fait pas uniquement en fonction de la distance des objets à la caméra, mais en fonction d'une combinaison de la distance de l'objet et de sa taille : les gros objets seront affichés avec un niveau de détail supérieur, à égale distance.

J'ai créé ce script pour imposer un changement uniquement en fonction de la distance, avec en tête une vue en 3 isométrique, de sorte que tous les objets de la carte changent de LOD en même temps en fonction du niveau de zoom. Toutefois, le script a été conçu pour fonctionner que la caméra soi en mode conique ou orthogonal. Il est inspiré initialement de ce script-là de Justin Diffenderfer, que j'ai modifié pour l'automatiser et le rendre applicable aux caméras orthogonales.

Il suffit d'attacher ce script à un objet contenant déjà un LOD Group, et le reste est automatique. Comme une taille d'écran ne se traduit pas immédiatement en distance, il a fallu prendre quelques hypothèses pour étalonner le modèle. En l'occurrence, pour la vue conique, la profondeur de champ de la caméra et la distance maximale de visibilité de l'objet (cull) du LOD Group. Pour le mode orthogonal, j'ai considéré arbitrairement qu'une taille de 1 montrait l'objet dans tout l'écran. On peut changer ce comportement en changeant la valeur de niveauQualitéSpécifique, cf. infra.

Les paramètres publics

public Camera cameraActive;
La caméra au regard de laquelle les LOD sont calculés. Initialisé à Camera.main si non renseigné.
Je n'ai pas prévu que la caméra puisse être changée à la volée en cours d'exécution puisque je n'en ai pas besoin dans mon projet, mais ça ne doit pas être très dur à réaliser.

public bool Cutoff = true;
(Inspiré de Justin Diffenderfer) "Vrai" si on veut que les objets soient cachés au delà de la limite de vision indiquée dans le LOD Group d'origine.

public int Skip = 4;
(Inspiré de Justin Diffenderfer) Valeur n indique que le script n'est exécuté qu'une fois toutes les n frames, utile pour économiser des efforts au processeur.

public int niveauQualitéSpécifique = 4;
Niveau de qualité réglable à la main pour chaque objet : un facteur multiplicateur à la distance à laquelle le niveau de détail change. Se comporte comme le facteur "LOD Bias" mais spécifique à l'objet.

Voila, ça a l'air de fonctionner, mais je ne crois pas avoir testé ce script dans toutes les situations possibles, merci de me dire s'il y a des erreurs ou des manques. Voire, de m'indiquer la correction :)

Code : Tout sélectionner

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

public class LOD_ortho : MonoBehaviour {

	List<Renderer[]> LOD_Renderer = new List<Renderer[]>();
	List<float> LOD_Distance = new List<float>(); // Distance à laquelle les objets changent de LOD.
	List<float> LOD_Ortho = new List<float>(); // Taille orthographique correspondante.

	public bool Cutoff = true; // Hide the object if it is beyond the maximum distance.
	public int Skip = 4;
	public Camera cameraActive;
	public int niveauQualitéSpécifique = 4;

	protected int _skipIndex = 0;
	protected int _currentLOD = 0;

	void Start()
	{
		if (!cameraActive)
		{
			cameraActive = Camera.main;
		}

		float profondeurDeChamp = cameraActive.farClipPlane - cameraActive.nearClipPlane;
		LODGroup leLODgroup = this.GetComponentInChildren<LODGroup> ();
		float Rcull = leLODgroup.GetLODs () [leLODgroup.lodCount-1].screenRelativeTransitionHeight; //La dernière limite, celle où on efface le sprite, "cull".
			
		foreach (LOD leLOD in leLODgroup.GetLODs())
		{
			LOD_Renderer.Add (leLOD.renderers);
			foreach (Renderer leRenderer in leLOD.renderers)
			{
				leRenderer.enabled = false;
			}
			LOD_Distance.Add(niveauQualitéSpécifique*QualitySettings.lodBias*Rcull*profondeurDeChamp/leLOD.screenRelativeTransitionHeight);
			LOD_Ortho.Add (niveauQualitéSpécifique*QualitySettings.lodBias*0.5f / leLOD.screenRelativeTransitionHeight); //nota : une taille orthographique de 1 correspond à un demi écran en hauteur. 0.5f pour 100% de l'écran couvert.
		}
		leLODgroup.enabled = false;

		//Le LOD zéro est affiché par défaut.
		foreach (Renderer leRenderer in leLODgroup.GetLODs () [0].renderers)
		{
			leRenderer.enabled = false;
		}
	}

	void Update() {
		// We don't need to update every frame, but we do need to update a few times every second.

		if (_skipIndex < Skip) {
			_skipIndex ++;
			return;
		}
		else {
			_skipIndex -= Skip;
		}

		float distance;
		if (cameraActive.orthographic) //CAS ORTHOGRAPHIQUE
		{
			// Maintenant, seule la taille orthographique de la caméra compte pour afficher les niveaux de détail.
			distance = cameraActive.orthographicSize;
			
			if (Cutoff)
			{
				if (distance >= LOD_Ortho [(LOD_Ortho.Count - 1)])
				{
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = false;
					}
					return;
				}
				else
				{
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = true;
					}
				}
			}

			// See if the LOD needs to be increased.
			if (LOD_Ortho.Count >= (_currentLOD + 1))
			{
				if (distance >= LOD_Ortho [_currentLOD])
				{
					// Change the mesh

					if (LOD_Renderer.Count >= (_currentLOD + 2))
					{
						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = false;
						}

						_currentLOD++;

						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = true;
						}
					}
				}
			}

			// Should it be decreased?

			if (_currentLOD > 0)
			{
				if (distance < LOD_Ortho [(_currentLOD - 1)])
				{
					// Change the mesh
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = false;
					}

					_currentLOD--;

					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = true;
					}
				}
			}
		}
		else //CAS PERSPECTIVE CONIQUE
		{
			//Je laisse tomber l'évitement des racines carrées
			distance = Vector3.Distance(cameraActive.transform.position, transform.position);

			if (Cutoff)
			{
				if (distance >= LOD_Distance [(LOD_Distance.Count - 1)])
				{
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = false;
					}
					return;
				}
				else
				{
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = true;
					}
				}
			}

			// See if the LOD needs to be increased.
			if (LOD_Distance.Count >= (_currentLOD + 1))
			{
				if (distance >= LOD_Distance [_currentLOD])
				{
					// Change the mesh

					if (LOD_Renderer.Count >= (_currentLOD + 2))
					{
						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = false;
						}

						_currentLOD++;

						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = true;
						}
					}
				}
			}

			// Should it be decreased?

			if (_currentLOD > 0)
			{
				if (distance < LOD_Distance [(_currentLOD - 1)])
				{
					// Change the mesh
					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = false;
					}

					_currentLOD--;

					foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
					{
						leRenderer.enabled = true;
					}
				}
			}
		}
	}
}

Avatar de l’utilisateur
boubouk50
ModoGenereux
ModoGenereux
Messages : 4031
Inscription : 28 Avr 2014 11:57
Localisation : Toulouse

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par boubouk50 » 14 Nov 2017 11:55

Merci pour le partage.
J'ai survolé un peu le code, en terme de performance, ça donne quoi?
"Ce n'est pas en améliorant la bougie, que l'on a inventé l'ampoule, c'est en marchant longtemps."
Nétiquette du forum
Savoir faire une recherche
Apprendre la programmation

Ran
Messages : 21
Inscription : 04 Déc 2015 10:43

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par Ran » 14 Nov 2017 12:04

boubouk50 a écrit :
14 Nov 2017 11:55
Merci pour le partage.
J'ai survolé un peu le code, en terme de performance, ça donne quoi?
La réponse courte c'est "j'en sais rien" :) Je l'ai collé à la main sur quelques objets dans ma scène et le nombre de fps ne bouge pour ainsi dire pas, mais c'est une petite scène avec peu d'objets, je n'ai pas essayé de tester ça en grand, faute d'avoir un monde de jeu suffisamment rempli pour l'instant...

Je ressens une vague culpabilité parce que le script dont je me suis inspiré possédait une fonction optionnelle pour éviter les racines carrées, alors que dans mon cas "conique" j'en appelle une avec Vector3.Distance. J'ai déconnecté cette fonction parce que ça introduisait une complexité dont je n'ai pas a priori besoin puisque j'utilise surtout la caméra orthogonale...

Avatar de l’utilisateur
boubouk50
ModoGenereux
ModoGenereux
Messages : 4031
Inscription : 28 Avr 2014 11:57
Localisation : Toulouse

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par boubouk50 » 14 Nov 2017 12:44

Ben disons que le LOD devient utile lorsque tu as une scène bien remplie, et donc ne pas afficher des polygones inutiles. Donc les tests sont à mener sur des grosses scènes.
Outre la distance à calculer, supprimer une racine carrée est intéressant puisque calculée pour chaque objet, je pensais plutôt aux foreach. Notamment

Code : Tout sélectionner

if (LOD_Renderer.Count >= (_currentLOD + 2))
					{
						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = false;
						}

						_currentLOD++;

						foreach (Renderer leRenderer in LOD_Renderer[_currentLOD])
						{
							leRenderer.enabled = true;
						}
					}
Dans le cas d'un nombre égal de LOD pour tous les objets, cela peut-être optimisé en une seule boucle.

Code : Tout sélectionner

if (LOD_Renderer.Count >= (_currentLOD + 2))
{
//length peut-etre définie au Start () pout ne pas avoir à la recalculer aussi
	int length = LOD_Renderer[_currentLOD].Count;
	for (int i = 0; i <=length; i++)
       	{
		leRenderer[_currentLOD][i].enabled = false;
		leRenderer[_currentLOD+1][i].enabled = true;
	}
	_currentLOD++;
}
Sinon il y a forcément plus d'éléments dans les niveaux inférieurs (ou supérieurs je ne sais pas comment tu procèdes) mais c'ets soit l'un soit l'autre. Ce code est faux, c'est juste pour montrer l'économie possible de bouclage.

Code : Tout sélectionner

if (LOD_Renderer.Count >= (_currentLOD + 2))
{
	int minLength = LOD_Renderer[_currentLOD].Count;
	int maxLength = LOD_Renderer[_currentLOD+1].Count;
	for (int i = 0; i <=minLength ; i++)
       	{
		leRenderer[_currentLOD][i].enabled = false;
		leRenderer[_currentLOD+1][i].enabled = true;
	}
	for (int i = minLength ; i <= maxLength ; i++) {
		leRenderer[_currentLOD+1][i].enabled = true;
	}
	_currentLOD++;
}
"Ce n'est pas en améliorant la bougie, que l'on a inventé l'ampoule, c'est en marchant longtemps."
Nétiquette du forum
Savoir faire une recherche
Apprendre la programmation

Ran
Messages : 21
Inscription : 04 Déc 2015 10:43

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par Ran » 14 Nov 2017 13:01

Approche intéressante. En l'occurrence le code d'origine était plus simple puisqu'il ne prévoyait qu'un seul renderer par LOD : je l'ai modifié pour le rendre plus rustique, à savoir pour un nombre quelconque de renderer par niveau de LOD. C'est vrai que je n'ai pas fait d'hypothèse sur la croissance ou la décroissance des nombres de renderer...

Est-ce que le fait d'appeler un foreach est coûteux en termes de performances ? Je n'ai pas beaucoup de pratique sur le sujet. Mais sur le principe, à part l'appel du foreach redondant, le nombre de renderers manipulés (dans la boucle update...) est strictement minimum : on déconnecte les anciens et on connecte les nouveaux.

J'avoue d'un autre côté qu'il y aurait de l'optimisation à faire dans la boucle start mais comme je suis un peu un flemmard et que c'est seulement une boucle "start", j'ai laissé en l'état :D

Avatar de l’utilisateur
boubouk50
ModoGenereux
ModoGenereux
Messages : 4031
Inscription : 28 Avr 2014 11:57
Localisation : Toulouse

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par boubouk50 » 14 Nov 2017 13:10

Ben c'est une boucle, donc pour chaque entrée, ça coûte.
Donc si tu as 10 000 éléments (pour les 2 cas), tu entres 20 000 fois alors que tu peux rentrer 10 000 fois, ça divise par 2, c'est pas rien.
"Ce n'est pas en améliorant la bougie, que l'on a inventé l'ampoule, c'est en marchant longtemps."
Nétiquette du forum
Savoir faire une recherche
Apprendre la programmation

Ran
Messages : 21
Inscription : 04 Déc 2015 10:43

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par Ran » 14 Nov 2017 14:20

boubouk50 a écrit :
14 Nov 2017 13:10
Ben c'est une boucle, donc pour chaque entrée, ça coûte.
Donc si tu as 10 000 éléments (pour les 2 cas), tu entres 20 000 fois alors que tu peux rentrer 10 000 fois, ça divise par 2, c'est pas rien.
Hmm... question intéressante, mais la réponse ne me semble pas si évidente. En l'occurrence le nombre d'opérations est le même : soit 10.000 boucles de 2 opérations chacune, soit 20.000 boucles d'une seule opération chacune... Il faudrait peut-être faire un benchmark, mais au stade où j'en suis dans mon projet, ça ressemblerait à de l'optimisation prématurée, qui est paraît-il le grand Satan de développement :)

Avatar de l’utilisateur
boubouk50
ModoGenereux
ModoGenereux
Messages : 4031
Inscription : 28 Avr 2014 11:57
Localisation : Toulouse

Re: [C#] Transformer LOD par tailles (standard) en LOD par distances

Message par boubouk50 » 14 Nov 2017 14:27

Tu n'as pas seulement l'instruction centrale, mais aussi l'incrémentation de l'indice i de la boucle et le test s'il est inférieur à la limite. Ceux-là, tu les économises.
Aussi, tu dois aussi avoir d'autres instructions liées à la déclaration de la boucle.
Et pour la compilation, surement du gain.
"Ce n'est pas en améliorant la bougie, que l'on a inventé l'ampoule, c'est en marchant longtemps."
Nétiquette du forum
Savoir faire une recherche
Apprendre la programmation

Répondre

Revenir vers « Scripts »