SOLVED Look At Camera Tag and text orientation

Hi,

I am generating a scene with with a python script, and want to add 3d text to label some positions.

I want the labels to maintain readable orientation when I change camera position, and added the 'Look at Camera' tag to the Text . This 'works', but the text is always reversed, i.e. the initial view of the text is from the back, and the tag now serves to maintain the reversed view of the text for any camera orientation.

When I create text in the editor the default orientation puts the text in XY plane, reading left-to-right along X when looking down the Z axis, so I made sure the camera was looking down Z when the scene was built, but the text is still reversed. Further experimentation leads me to believe that initial camera position has no effect on the text orientation. (There is also a 'reverse' property of the text object, does not seem to have any effect.)

I am appending the code I am using below.

Thanks in advance,

Randy

extrude = c4d.BaseObject(5116)
extrude[c4d.EXTRUDEOBJECT_EXTRUSIONOFFSET] = textheight/2.
extrude.SetBit(c4d.BIT_ACTIVE)

text = c4d.BaseObject(5178)
text[c4d.PRIM_TEXT_TEXT] = 'my label'
## center
text[c4d.PRIM_TEXT_ALIGN] = 1
text[c4d.PRIM_TEXT_HEIGHT] = textheight 
# this experiment had no effect
#text[c4d.PRIM_REVERSE] = 1
#
ttag = c4d.TextureTag()
ttag.SetMaterial(sceneMaterials['H'])
text.InsertTag(ttag)

looktag = c4d.BaseTag(1001001)
text.InsertTag(looktag)

text.SetBit(c4d.BIT_ACTIVE)
text.InsertUnder(extrude)

## this gets target position in scene
pos = numpy.array(mol['atomsInOrder'][theIDX][1])
# convert left-handed coords 
pos[2] = -pos[2]
width = text.GetRad()[1]
# compute displacement to put center of text box at CA position
disp = pos - numpy.array((0.,textheight/2.,0.))
#
extrude.SetAbsPos(disp.tolist())

doc.InsertObject(extrude)

Hello @zauhar,

Thank you for reaching out to us. Please note that we ask for executable code in our Forum Guidelines and using numpy and non-exposed data sets (your molecule data set) does not qualify as such. It works in this case for use, but it might not in your next case. Please adhere to the forum guidelines.

Problem

This is not really a API problem, but more a problem of the app itself or a user problem. A Text Spline object is constructed with the z/k/v3-axis facing from the backside of the spline and there is nothing you can do about it (except for inverting the coordinate system of the object itself, which is not advisable as a basis with a negative component, i.e., a negative axis, can lead to other problems). Because both the Look at Camera and Target Expression orient things towards the positive z-axis, you will be then looking at "the back-side of the spline". The option PRIM_REVERSE will not change anything about this because it is part of a standard spline description and is used to invert the point order of the spline; it does not invert the geometry itself.

Solutions

You have two viable solutions:

  1. Use Constraint Tag from the character toolset. It also has a target functionality. Here you can pick the axis along which the orientation should happen. You will also need to enable the up-vector option of the tag for the orientation to be carried out as you want it to be.
    c5d97a97-fc4f-4ab0-a56d-6a5cb43de933-image.png
    2.You can also do it yourself by implementing a Programming Tag (which then your script would create). Depending on how comfortable you are with transforms, this is a fairly simple thing to do, and you have full control over what happens. We regularly have questions for how this can be done, the most recent one can be found here.

Cheers,
Ferdinand

@ferdinand

Ferdinand thanks for the explanation and suggestions.

I tried the constraint tag approach first. (I understand and prefer the 'programmatic approach' you also shared, but don't understand the 'Optional' stuff at this point, which I assume involves interactive object selection??)

When I paste the code below into the console, I get text with associated constraint but no tracking behavior (I can orbit around the text).

I am puzzled that when I examine the constraint tag in the editor, the target is not initially set, neither for aim nor up-axis. If I simply run the assignments again (constrainttag[20001] = camera, etc) they immediately show up in the editor, but still no tracking behavior.

I am clearly missing one or more important points.

Thanks for any advice,
Randy

textheight = 10.
extrude = c4d.BaseObject(5116)
extrude[c4d.EXTRUDEOBJECT_EXTRUSIONOFFSET] = textheight/2.
extrude.SetBit(c4d.BIT_ACTIVE)
text = c4d.BaseObject(5178)
text[c4d.PRIM_TEXT_TEXT] =  'Hello'

text[c4d.PRIM_TEXT_ALIGN] = 1
text[c4d.PRIM_TEXT_HEIGHT] = textheight 
text.InsertUnder(extrude)
doc.InsertObject(extrude)

camera = doc.GetRenderBaseDraw().GetSceneCamera(doc)
constrainttag = c4d.BaseTag(1019364)

# set objects for aim and up axis

constrainttag[20001] = camera 
constrainttag[40001] = camera 

constrainttag[c4d.ID_CA_CONSTRAINT_TAG_UP] = 1
constrainttag[c4d.ID_CA_CONSTRAINT_TAG_AIM] = 1

# aim -Z

constrainttag[20004] = 5

# up axis +Y

constrainttag[40004] = 1 

# up axis when aiming along 

constrainttag[40005] = 5

text.InsertTag(constrainttag)

(If the hierarchy is not so much of an issue, you could also just set a null as parent of the text. The null will then get the look-at-camera tag so its z-axis points at the camera. The text will be a child of the null, and you turn it by 180° so its z axis points away from the camera in the opposite direction as its parent. I am not totally sure the look-at-camera is always well behaved, though.)

Hey @zauhar,

The main problem in your example is that your code does not enable the camera dependent option of the expression.

44b5ae65-f573-482c-b8fd-fc4ae2985584-image.png

To do that, you must deal with the c4d.PriorityData which wraps that value. Find a simple example at the end of the posting.

I understand and prefer the 'programmatic approach' you also shared, but don't understand the 'Optional' stuff at this point, which I assume involves interactive object selection??)

I am not quite sure what you are talking about, you mean the typing.Optional? typing is just a module for type assertions in modern Python and in the process of becoming the standard, with a more strictly typed Python being the goal. We are already orienting our code examples towards that future, and because this also makes code more readable. typing.Optional just means that something is optional, i.e., can also be None. E.g., def foo(a: int, b: typing.Optional[int]) -> typing.Optional[bool]: means that foo can either take an int as b or None and returns either a bool or None.

But in the relevant code example the import was not required, and it was only there because it is part of the default script and I forgot to remove it. I have removed it now to avoid further confusion.

Cheers,
Ferdinand

The code:

"""Adds a camera dependent constraint tag to a text object.

The reason why your script did not work was because you did not enable the camera dependence of
your expression.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument  # The active document
op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected

TEXT_HEIGHT: int = 10 # The height of text in world units.

def AssertType(item: any, t: typing.Union[typing.Type, tuple[typing.Type]], label: str) -> any:
    """Asserts #item to be of type #t.

    When the assertion fails, a TypeError is risen. Otherwise, #item is being passed through.
    """
    if not isinstance(item, t):
        raise TypeError(f"Expected {t} for '{label}'. Received: {type(item)}")
    
    return item

def main() -> None:
    """
    """
    # It is really important to check things for not being the nullptr, or the Python equivalent,
    # None, in the classic API. 
    myCube: c4d.BaseObject = c4d.BaseObject(c4d.Ocube)
    if myCube is None:
        raise MemoryError(f"Could not allocate cube.")

    # Which can get a bit tedious to write, so I often use something like the function AssertType,
    # which has been provided above. You could also use typing_extensions.assert_type() instead.
    
    extrude: c4d.BaseObject = AssertType(c4d.BaseObject(c4d.Oextrude), c4d.BaseObject, "extrude")
    extrude[c4d.EXTRUDEOBJECT_EXTRUSIONOFFSET] = TEXT_HEIGHT * 0.5
    extrude.SetBit(c4d.BIT_ACTIVE)

    text: c4d.BaseObject = AssertType(c4d.BaseObject(c4d.Osplinetext), c4d.BaseObject, "text")
    text[c4d.PRIM_TEXT_TEXT] = "Hello"
    text[c4d.PRIM_TEXT_ALIGN] = c4d.PRIM_TEXT_ALIGN_MIDDLE
    text[c4d.PRIM_TEXT_HEIGHT] = TEXT_HEIGHT
    text.InsertUnder(extrude)
    doc.InsertObject(extrude)

    # This is especially true in these calls, the active render BaseDraw can be None.
    doc.ForceCreateBaseDraw()
    bd: c4d.BaseDraw = AssertType(doc.GetRenderBaseDraw(), c4d.BaseDraw, "bd")
    camera: c4d.BaseObject = AssertType(bd.GetSceneCamera(doc), c4d.BaseObject, "camera")

    # Instead of instantiating and then inserting the tag, you can also just use MakeTag(). The
    # character animation stuff is unfortunately lacking symbols left and right, so we have to
    # use the raw integer here.
    tag: c4d.BaseObject = AssertType(text.MakeTag(1019364), c4d.BaseTag, "tag")

    tag[c4d.ID_CA_CONSTRAINT_TAG_UP] = True
    tag[c4d.ID_CA_CONSTRAINT_TAG_AIM] = True

    # These are the only two parameters we must write, the target object and the axis. Here the
    # symbols are also not available, but for a different reason. This part of the description is
    # dynamic (the user can add or remove targets). This is usually solved with stride symbols,
    # e.g., tag[SOME_STRIDE * tag.GetItemCount() + OFFSET_PARAMETER_1], but the CA Team did not
    # even expose the strides. I could tell them you, but this won't help much in this case, so
    # we are just going to hard-code the values.
    tag[20001] = camera
    tag[20004] = 5

    # The important bit, you must enable the camera dependence of the tag.
    priority: c4d.PriorityData = AssertType(
        tag[c4d.EXPRESSION_PRIORITY], c4d.PriorityData, "priority")
    priority.SetPriorityValue(c4d.PRIORITYVALUE_MODE, c4d.CYCLE_EXPRESSION)
    priority.SetPriorityValue(c4d.PRIORITYVALUE_CAMERADEPENDENT, True)
    tag[c4d.EXPRESSION_PRIORITY] = priority

    # This is important and was missing in your script, as otherwise the GUI won't update until
    # the user interacts with it in some form.
    c4d.EventAdd()

if __name__ == '__main__':
    main()

Ferdinand, thanks for the detailed reply, now it is working correctly. I did not realize the importance of the priority setting.

I may have been tricked by BaseDraw not being initially defined, perhaps that is why my initial assignment of the camera as target did not 'take' and I needed to do it again.

About the 'optional stuff' - just misinterpretation on my part. I see 'op' in code examples like the one you pointed me to,

https://plugincafe.maxon.net/topic/14117/gimbal-lock-safe-target-expression/2

which seems to come from a global context, so I assumed represented a selected object. I also see Optional being used to wrap return values, I see this implements something akin to optional variables in Swift. So I assumed 'op' was shorthand for an optional value.

I have been coding long enough that I can conflate and confabulate with the best of them. ;-(

Cairyn's solution above also works, by the way.

Thanks!! I will mark as solved

@cairyn

Thanks very much, your solution and Ferdinand's below both work.

Randy

Hey @zauhar,

which seems to come from a global context, so I assumed represented a selected object. I also see Optional being used to wrap return values, I see this implements something akin to optional variables in Swift. So I assumed 'op' was shorthand for an optional value.

op is a really, really, realllllllly old Cinema 4D naming convention and stands for object pointer. In classic API plugin interfaces, op usually stood for something that is representing the plugin on the user/GUI layer. E.g., for an ObjectData plugin there are multiple methods which pass in an argument op which then is the BaseObject representing the plugin hook in the Object Manger.

Over time this has been watered a bit down. In a Script Manger script, op is simply the currently selected object (which can be None), and Cinema 4D will write that as a module attribute into the module before it is being executed (just as doc for the currently active document).

Cheers,
Ferdinand

Ah Ok, thanks Ferdinand.

At least I was partly on base.

Randy

@ferdinand

I have to add an update to this. While I marked this as 'solved', in fact initially I was not looking carefully at behavior in the view.

While the text stayed 'more or less' vertical with respect to camera orientation sometimes it was far off, depending on the specific camera angle.

I looked at the :Look At Camera python example, and the approach is to set Z as the normalized displacement from host to target, and set 'up' as the global Y axis , then generate normalized X and Y by taking cross products.

I made a modified plugin, and changed the math as follows:

def GetLookAtTransform(host: c4d.Matrix, target: c4d.Matrix) -> c4d.Matrix:
    """Returns a transform which orients its z/k/v3 component from #host to #target.
    """
    # Get the position of both transforms.
    p: c4d.Vector = host.off
    q: c4d.Vector = target.off

    # The normalized delta vector will become the z-axis of the frame.
    #z: c4d.Vector = ~(q - p)
    # I reversed this as my application is for a text spline, where you look down the negative z-axis of 
    # the object in default orientation
    z: c4d.Vector = ~(p - q)

    # We compute an up-vector which is not (anti)parallel to #z.
    #up: c4d.Vector = c4d.Vector(0, 1, 0) \
    #                 if 1. - abs(z * c4d.Vector(0, 1, 0)) > EPSILON else \
    #                 c4d.Vector(EPSILON, 1, 0)
    #
    # Instead, take camera Y and orthonormalize with respect to our Z
    y = ~(target.v2 - (target.v2 * z) * z)
    # 
    #x: c4d.Vector = ~(up % z)
    # get x using cross product 
    x: c4d.Vector = ~(y % z)
    #y: c4d.Vector = ~(z % x)

    # Return the frame (x, y, z) plus the offset of #host as the look-at transform.
    return c4d.Matrix(off=p, v1=x, v2=y, v3=z)

This now keeps the text orientation nailed, aligned with the 'up' axis of the camera.

I have not handled the pathological case of the initial Z being directly along the camera Y axis, I will take a look at that.

Sorry if this is solved somewhere else and I did not see it. If not I will try to submit the plugin.

Thanks,

Randy

Hey @zauhar,

Thank you for reaching out to us and sharing your code. I personally agree that constructing the frames yourself is usually best as you can do whatever you want there. I answered here in the form chosen by you, already existing tags, as users often prefer not dipping into transforms when they can avoid it.

You can also choose/inherit your up-vector from something else, e.g., the thing, which is being targeted, and have something akin to the idea of parallel transport. But as you said yourself, this code of yours now could produce collapsed vectors when your y happens to be (anti)parallel to z. The code which you commented out for up contains the common approach to test for that and to prevent it (you must keep floating point precision in mind).

Cheers,
Ferdinand

@ferdinand

Thanks, that is exactly correct - in fact I did not need to make the orthonormal vector, all one needs do is set the objects Y-axis equal to the camera, and get the x-axis by cross product .

I think I correctly handle the pathological situation after looking at the limiting cases.

The modified function is below.

Randy

EPSILON = 1E-5 # The floating point precision we are going to assume, i.e., 0.00001

# This is based on the python Look at Camera example
# https://github.com/PluginCafe/cinema4d_py_sdk_extended/tree/master/plugins/py-look_at_camera_r13

def GetLookAtTransform(host: c4d.Matrix, target: c4d.Matrix, reverseZ=True) -> c4d.Matrix:
    """Returns a transform which orients its z/k/v3 component from #host to #target.
    """
    # Get the position of both transforms.
    p: c4d.Vector = host.off
    q: c4d.Vector = target.off

    # The normalized offset vector between 'host' (object to be reoriented) and 'target' (the camera)
    # will become the z-axis of the modified frame for the object .
    #
    # If reverseZ = True, the new z-axis is points from camera toward object, if False the reverse
    # I turn reverseZ on by default, as my initial application is to text splines, which are meant to be
    # viewed looking down the object z-axis.
    # In the original implementation
    # (https://github.com/PluginCafe/cinema4d_py_sdk_extended/tree/master/plugins/py-look_at_camera_r13 )
    # the modified y-axisis computed using the global y-axis, and this does not consistently
    # keep the text upright in the view of the camera. Instead, simply take the object y-axis same as camera y.
    #
    # In the pathological case of new object z-axis parallel to camera y :
    # If reverseZ :  set object z = camera y , object y = -camera Z
    # else : set object z = -camera y, object y = -camera z
    #
    if reverseZ :
         z: c4d.Vector = ~(p - q)
         if 1. - abs(z * target.v2) > EPSILON :
             y = target.v2
         else :
             z = target.v2
             y = -target.v3
    else :
         z: c4d.Vector = ~(q - p)
         if 1. - abs(z * target.v2) > EPSILON :
             y = target.v2
         else :
             z = -target.v2
             y = -target.v3

    # get x using cross product
    x: c4d.Vector = ~(y % z)

    # Return the frame (x, y, z) plus the offset of #host as the look-at transform.
    return c4d.Matrix(off=p, v1=x, v2=y, v3=z)