- Home /
The question is answered, right answer was accepted
[Solved] Put dynamic labels around 3D model
Hello everyone ! :)
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 :
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 ! :)
PS : Sorry for bad english ! :D
Hello Can you Please explained more in detail if possible because I don't understand the Problem solution very well. I need step by step tutorials how to place Dynamic label around the 3D Object and how to put labels on 3D object. Please guide me as I am newbie to AR Development and not having much experience about it.
Answer by Scribe · Jan 29, 2015 at 07:26 PM
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[i] = new InterestPoint(psc[i]);
}
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 = radiusScale*c.GetComponent<MeshFilter>().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>();
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
textObject = label.GetComponent<Text>();
textObject.text = text;
textObject.horizontalOverflow = HorizontalWrapMode.Overflow;
textObject.verticalOverflow = VerticalWrapMode.Overflow;
if(point.GetComponent<LineRenderer>() == null){
lineRenderer = point.AddComponent<LineRenderer>();
}else{
lineRenderer = point.GetComponent<LineRenderer>();
}
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>();
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
textObject = label.GetComponent<Text>();
textObject.text = text;
textObject.horizontalOverflow = HorizontalWrapMode.Overflow;
textObject.verticalOverflow = VerticalWrapMode.Overflow;
if(point.GetComponent<LineRenderer>() == null){
lineRenderer = point.AddComponent<LineRenderer>();
}else{
lineRenderer = point.GetComponent<LineRenderer>();
}
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.normalized*r)+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
Ooha ! =O Thank you Scribe ! I didn't expected a solution that complete ! I will give it a try this week-end ! =D
Haha, I wasn't really planning on writing a full solution, but I'm still rather unfamiliar with the new UI system myself and thought this might be somewhat of an interesting way to learn a little more ^^. Hope you get it to work,feel free to ask any questions, I realise its probably a little hard to follow as I haven't commented anything (bad habits)
Hey guys, is there anyway I could get a real beginner step-by-step on how you implemented this? I am brand new to unity and what you did here is exactly what I'm looking for but have no idea how to add it. :( Any help would be appreciated. Thanks!
Have you read through everything I've written, it is quite a complete guide already. Which parts are you struggling to understand? I am not writing a step-by-step guide on how to create a new c# file in unity.
please tell us where should the code to attach ? or can you make us a tutorial like Hellium create ? thankyou
@Scribe i'm new to unity modelling , i'm using vuforia sdk image target example to identify and image from camera screen , i have managed to identify the target and place a 3d text on top of , bow my requirement is place a label on top of it , let s a 2 d text or ui label . how can i use your explanation for it ???
Hi @Scribe this seems like a really great solution. It may seem like its not very neat as you said, but it also seems to contain everything needed to get the job done! $$anonymous$$udos to you!
I would like to implement this solution (or something similar) in a project I am building and need some assistance. I have created an "InterestPointClasses.cs" file that inherits from monobehavior, and a "Controller.cs" file that inherits from InterestPointClasses. I am not getting any errors in the Controller.cs file, so the inheritance is working properly.
I need some clarification on the following:
How do I actually place a new InterestPoint from a click/touch?
How do I set text onto the new InterestPoint?
$$anonymous$$y project may have an element in AR, and so the camera (phone's camera) would be moving constantly. Do I need to use pointGroup.On$$anonymous$$ove() to handle those camera movements? How does On$$anonymous$$ove work/how do I use it?
I imagine I am just missing a few key bits of information that may be preventing me from answering all my questions on my own, so any information you can provide would be greatly appreciated. I have read through all your code and your notes, as well as comments on this thread and I haven't been able to figure it out yet. I'm not as proficient in Unity/C# as you clearly are :).
Answer by Hellium · Feb 03, 2015 at 01:58 PM
I finally took the time to test your script ! It rocks !! =D Thank you soo much Scribe ! =D
Glad it worked well for you! And thank you for co$$anonymous$$g back to accept the answer, it is much appreciated :) Have fun!
Hi Scribe,
I was able to integrate your code with success too. I have a ![alt text][1]question hopefully you can help. I am using your code for an AR app. If a have more than one text, they can overlap each other. I am attaching the url for the picture. How would you solve this problem?
Thanks
Hello charly, I was able to run thee scripts and added the said things in the inspector but I couldn't see the rendered line and the label attached to the centralObject when I finally run. Can you help me where I am missing?
Would anyone be able to explain to me what skills I would need to acquire in order to implement this? I am looking to create a proof of concept and this is exactly what I'm looking for. I am able to get everything into unity, now I want to tag certain areas of my model for customers to use as references on our hardware. An implementation demo or guidance would be greatly appreciated.
Hello Helium, I was able to run thee scripts and added the said things in the inspector but I couldn't see the rendered line and the label attached to the centralObject when I finally run. Can you help me where I am missing?
The project I was working on is more than 5 years old. Unfortunately, I don't have it anymore, I'm sorry.
It is an old question, but answering @chavijain1601_unity:
To make the LineRenderer to show up you need to follow exactly what @Scribe instructs, which is: add one object as the main object (let's say, a Cube). Create another 3D object as child of this main object (it can be a sphere to look like the picture posted by @Hellium. And then create another empty 3D object, lets say, called "Label3D", that receives the Monobehaviour (code above that has the Start and Update methods). This monobehaviour would point its "Central Object" variable to the 3d object that is child of the main object. I was having the same problem of not showing up the LineRenderer and, after doing the above steps, it showed up correctly.
Hello Buddy, I wanted to know more abt this issue in detail as I am building one application based on AR and it needs Labelling as soon as we tap on that Object. So I am not getting much help from internet and I found you. Please help me with this
[1]: https://drive.google.com/file/d/1k0Qs0v0_8CpLw5JUYmM0_25Rb0dTFNmS/view?usp=sharing