[Solved] Put dynamic labels around 3D model

Hello everyone ! :slight_smile:

I would like to arrange multiple text labels around a 3D model to point out specific points of it. These labels should not fly over the model but stay around it, clamped on an “imaginary circle” which belongs to the plane normal to the vector Origin-Camera. Also, I would like to link these labels with the points of interest ( LineRenderer is the appropriate tool I guess).

It could be “simple” if my model would stay unmoving, but unfortunately, it won’t.

Information known

  • Size of the model (fixed at a value of 100) => diameter of my imaginary circle
  • Position of the 3D model’s center (we will assume here it is the origin : Vector3.zero )
  • Position of the points of interest
  • Position of my camera ( Camera.main.transform.position )
  • Target of my camera (we will assume here it is the 3D model’s center : Vector3.zero )

You will better understand what I mean with an image I guess :

![1]

I will explain the two different ways I tried so as to achieve what I want to do, maybe, I have missed something … In both cases I worked with a Canvas whith a Render Mode fixed with Screen Space - Overlay. The C# scripts below are attached to Text prefabs (Unity 4.6 GUI) I instantiate dynamically from a database.


Working in 2D

Since I want to put labels on the screen, I will work in 2D as soon as possible.

	void Update( )
	{
		// Get the position of the origin on the screen
		Vector3 screenPointOrigin = Camera.main.WorldToScreenPoint( Vector3.zero );              

		// Get the position of the point of interest on the screen
		Vector3 screenPointInterest = Camera.main.WorldToScreenPoint( pointOfInterestSphere.transform.position );

		// Get the radius of the imaginary circle :
		// Get the point on the top of it and compute the distance from the center of the circle
		// 100 = model's size
		float circleRadius = ( Camera.main.WorldToScreenPoint( Camera.main.transform.up * 100 ) - screenPointOrigin).magnitude;
    
		// Arrange the label around the model
		transform.GetComponent<RectTransform>().anchoredPosition = new Vector2(
			( screenPointInterest - screenPointOrigin ).normalized.x * circleRadius,
			( screenPointInterest - screenPointOrigin ).normalized.y * circleRadius
		);
	}

Here, the script arrange the labels around the imaginary circle, but the circle is centered on the middle of my screen and not on the center of my model (Note that I don’t move my camera in any of my scripts and the center of the model is (0, 0, 0) ).

Another problem is the Line Renderer which is nearly impossible to do since my labels are on my screen. When I tried to draw the Line Renderer, it hits the screen and makes a disgusting aspect.


Working in 3D

In 3D, the script is different since I work with the position of my labels in the 3D space. But before my explanation, I invite you to read this page about the projection of a vector onto a plane.

void Update( )
	{
		// Get the projection vector onto the plane normal to the vector Origin-Camera
		transform.position = pointOfInterest.transform.position - Camera.main.transform.position * Vector3.Dot( pointOfInterest.transform.position, Camera.main.transform.position ) / Camera.main.transform.position.sqrMagnitude ;

		// Clamp the label on the imaginary circle and readjust the position at the center of the canvas
		// 100 = model's size
		transform.position = transform.position.normalized * 100 ;
	transform.position = transform.position + GameObject.Find("SculptureCanvas").transform.position ;
	}

Here, same problem, the labels are not centered around the model and they don’t gravitate well around the model (I know I have to consider the distance model-camera). Moreover, the Line Renderer is nearly impossible to do… With a Render Mode fixed to Screen Space - Camera, the script is nearly the same, and the LineRenderer is perfect, but the labels are not quite arranged.


Thank you in advance ! I hope my explanation is quite clear ! :slight_smile:

PS : Sorry for bad english ! :smiley:

Firstly +1 for a very nicely thought out and well presented question, there are too many users who don’t put in any effort and still expect free support ^^.

My solution is I guess, not very neat… but it works!

The main piece of code I will post is a set of classes to handle a lot of the nasty code outside of anything you attach to an object, the 3 classes are ‘InterestPoint’ which is a class to handle a single point of interest, its label and linerenderer. ‘InterestPointConstructor’ which is a serialized class to just make it easier to assign a lot of these labels from the inspector but still have the automated set-up that you get from calling new InterestPoint(...). Finally we have ‘InterestPointGroup’ which as you might guess, handles a group of InterestPoints and saves information about the central object, camera and ‘radiusScale’ which is how much bigger the circle is compared to the mesh bounds, 1.5 to 2 will probably work nicely for you in most cases.

public class InterestPointGroup {
	GameObject c;
	float radius;
	public float radiusScale;
	public InterestPoint[] points;
	public Camera camera;
	
	public InterestPointGroup(GameObject go, float rScale, params InterestPoint[] ps){
		radiusScale = rScale;
		centralObject = go;
		points = ps;
		camera = Camera.main;
		OnMove();
	}
	public InterestPointGroup(GameObject go, float rScale, params InterestPointConstructor[] psc){
		radiusScale = rScale;
		centralObject = go;
		camera = Camera.main;
		
		points = new InterestPoint[psc.Length];
		for(int i = 0; i < psc.Length; i++){
			points _= new InterestPoint(psc*);*_

* }*
* OnMove();*
* }*

* public void OnMove(){*
* Vector3 v;*
* Vector3 cScreenSpace = camera.WorldToViewportPoint(c.transform.position);*
* foreach(InterestPoint p in points){*
* v = p.CirclePosition(camera, c, radius);*
* v = camera.WorldToViewportPoint(v);*
* InterestPoint.MoveLabel(p, v, v-cScreenSpace);*
* }*
* }*

* public GameObject centralObject{*
_ set { c = value; radius = radiusScalec.GetComponent().mesh.bounds.extents.magnitude; }_
_
get { return c; }_
_
}_
_
}*_

public class InterestPoint {
* public string text;*
* public GameObject point;*
* public GameObject label;*
* public LineRenderer lineRenderer;*
* public RectTransform rectTransform;*
* Vector3 circlePoint;*

* public Text textObject;*

* public InterestPoint(string s, GameObject go, GameObject l){*
* text = s;*
* point = go;*
* label = l;*
* rectTransform = label.GetComponent();*
* rectTransform.offsetMin = Vector2.zero;*
* rectTransform.offsetMax = Vector2.zero;*

* textObject = label.GetComponent();*
* textObject.text = text;*
* textObject.horizontalOverflow = HorizontalWrapMode.Overflow;*
* textObject.verticalOverflow = VerticalWrapMode.Overflow;*
* if(point.GetComponent() == null){*
* lineRenderer = point.AddComponent();*
* }else{*
* lineRenderer = point.GetComponent();*
* }*
* lineRenderer.SetVertexCount(2);*
* lineRenderer.useWorldSpace = false;*
* lineRenderer.SetPosition(0, Vector3.zero);*
* lineRenderer.SetWidth(0.1f, 0.1f);*
* }*

* public InterestPoint(InterestPointConstructor psc){*
* text = psc.text;*
* point = psc.gameObject;*
* label = psc.label;*
* rectTransform = label.GetComponent();*
* rectTransform.offsetMin = Vector2.zero;*
* rectTransform.offsetMax = Vector2.zero;*

* textObject = label.GetComponent();*
* textObject.text = text;*
* textObject.horizontalOverflow = HorizontalWrapMode.Overflow;*
* textObject.verticalOverflow = VerticalWrapMode.Overflow;*
* if(point.GetComponent() == null){*
* lineRenderer = point.AddComponent();*
* }else{*
* lineRenderer = point.GetComponent();*
* }*
* lineRenderer.SetVertexCount(2);*
* lineRenderer.useWorldSpace = false;*
* lineRenderer.SetPosition(0, Vector3.zero);*
* lineRenderer.SetWidth(0.1f, 0.1f);*
* }*

* public Vector3 CirclePosition(Camera cam, GameObject c, float r){*
* Vector3 fwd = cam.transform.forward;*
* Vector3 v = point.transform.position-c.transform.position;*
* v -= Vector3.Project(v, fwd);*
_ v = (v.normalizedr)+c.transform.position;_
_
lineRenderer.SetPosition(1, point.transform.InverseTransformPoint(v));_
_
circlePoint = v;_
_
return v;_
_
}*_

* public static void MoveLabel(InterestPoint p, Vector3 v, Vector3 off){*
* p.rectTransform.anchorMin = v;*
* p.rectTransform.anchorMax = v;*

* if(off.x > 0){*
* if(off.y > 0){*
* p.textObject.alignment = TextAnchor.LowerLeft;*
* }else{*
* p.textObject.alignment = TextAnchor.UpperLeft;*
* }*
* }else{*
* if(off.y > 0){*
* p.textObject.alignment = TextAnchor.LowerRight;*
* }else{*
* p.textObject.alignment = TextAnchor.UpperRight;*
* }*
* }*

* }*

* public Vector2 viewportPosition{*
* get { return Camera.main.WorldToViewportPoint(point.transform.position); }*
* }*
* public Vector3 target{*
* get { return point.transform.position; }*
* }*
* public Vector3 circlePosition{*
* get { return circlePoint; }*
* }*
}

[System.Serializable]
public class InterestPointConstructor {
* public string text;*
* public GameObject gameObject;*
* public GameObject label;*
}
----------
So now you have those classes here is how you can use them nice and simply:
public GameObject centralObject;
public InterestPointConstructor[] points;
public InterestPointGroup pointGroup;

void Start(){
* pointGroup = new InterestPointGroup(centralObject, 2, points);*
}

void Update(){
* pointGroup.OnMove();*
}
preferably you would only call pointGroup.OnMove() when you change you camera or model orientation (on user input normally), this would save quite a bit of computation as opposed to calling it in update, but this works well for testing purposes.
In the inspector you should set up you list of InterestPointConstructor so that the ‘text’ field is what you want displayed in the label, the gameObject is an object (probably empty) set to the position of your interest point and set as a child of your main central object and finally the ‘Label’ variable should be set to an object containing a RectTransform and Text component or you will recieve errors!
The rest should be handled by the auto setup of the InterestPoint class.
I hope that helps
Scribe

I finally took the time to test your script ! It rocks !! =D
Thank you soo much Scribe ! =D

40069-annotations.png