Look At Camera Tag and text orientation
-
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,
Randytextheight = 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.
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 beNone
. E.g.,def foo(a: int, b: typing.Optional[int]) -> typing.Optional[bool]:
means thatfoo
can either take anint
asb
orNone
and returns either abool
orNone
.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,
FerdinandThe 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://developers.maxon.net/forum/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
-
-
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 theBaseObject
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
-
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 toz
. The code which you commented out forup
contains the common approach to test for that and to prevent it (you must keep floating point precision in mind).Cheers,
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)