How to convert the length of a line on the screen to the length of an object.
-
hi,
I tried to have an object display its coordinate axis at a fixed screen space length, but it didn't work properly. Where did I make a mistake with my code?This is a c4d file that displays its coordinate axis using tags.-->
video:
code:
import c4d doc: c4d.documents.BaseDocument # The document containing this field object. op: c4d.BaseTag # The Python tag containing this code. flags: int # The execution flags of `main()`. See c4d.EXECUTIONFLAGS for details. priority: int # The execution priority of this tag. See c4d.EXECUTIONPRIORITY for details. tp: c4d.modules.thinkingparticles.TP_MasterSystem # The TP system of the document. def main() -> None: """Called by Cinema 4D to execute the tag. """ # Get the object the tag is attached to and its global position. pass def draw(bd: c4d.BaseDraw) -> bool: # Called to display some visual element in the viewport. Similar to TagData.Draw. # Write your code here if op[c4d.EXPRESSION_ENABLE]: size = 40 # 40 pixel length line bd.SetMatrix_Screen() start = c4d.Vector(200) end = start + c4d.Vector(size,0,0) bd.SetPen(c4d.Vector(1.0)) bd.DrawLine2D(start, end) mg = op.GetMain().GetMg() pos = bd.WS(mg.off) length = size/bd.WP_W(pos,True) bd.SetPen(c4d.Vector(1.0,0,0)) x = bd.WS(mg * c4d.Vector(length,0,0)) bd.DrawLine2D(pos,x) bd.SetPen(c4d.Vector(0,1.0,0)) y = bd.WS(mg * c4d.Vector(0,length,0)) bd.DrawLine2D(pos,y) bd.SetPen(c4d.Vector(0,0,1.0)) z = bd.WS(mg * c4d.Vector(0,0,length)) bd.DrawLine2D(pos,z) return True
Thanks for any help!
-
Hey @chuanzhen,
Thank you for reaching out to us. I do not really understand what your code is meant to do. The line
length = size/bd.WP_W(pos,True)
makes for example no sense to me, as you are here trying to get the size of a world space unit in screen space units, be that the divisor of a constant you defined. And as the input you use the position of your object, to then later use that value as the size of your axis gizmos.Perspective projections, i.e., functions such as
BaseView.WS
, are non-linear transforms. Other than the linear transforms carried out by Cinema 4D'sMatrix
class, they do not preserve length relations. In less fancy words: When you project the three axis of a coordinate system from world space (where each has the length of 1) into screen space, you will end up with three vectors with three different lengths. I.e., the operation is non-linear, as it does not preserve the length relations between things one feeds into it; before x, y, z were all of the same length, now they are not anymore. Your code ignores this fact when you mix up world and screen space values in a line as suchx = bd.WS(mg * c4d.Vector(length,0,0))
(length
is used as a world space component but has been constructed with screen space values).I think what you want to do here, is draw a 'thing', e.g., an axis gizmo in a size that is independent of the position of the object in world space. You can do two things here:
- Project the 'thing' into screen space and move and normalize it there. One way could be for example to project all vectors into screen space, and then normalize their bounding box, i.e., make the box which encompasses them always the same size. But this would result in a "unsteady" animation, when carried out incorrectly, as the width of the drawing shrinks and grows when the object is rotated around its y-axis (as the x-axis rotates in and out of vision). So, you would have to normalize over the width and height (pick the bigger), and rather then simple normalizing, would have to clamp the bounding box, so that its width and height cannot larger than a value X. This can all get non-trivial quite fast.
- The much easier option is just do project the object from a fixed distance and position, which would allow you that 'fixed size'. But also here perspective distortions can be an issue when you then additionally move the drawing (as I did below). This is why such things are usually done in a parallel projection and not a perspective projection.
The underlying answer is therefore that what you are trying to do, is conceptually impossible, or at least very much non-trivial.
Cheers,
FerdinandResult
Code
One could do here more to anchor the axis gizmo better to the top left corner of the viewport, I just pick
(100, 100, 0)
in screen space as the anchor, ignoring all current view frame and safe frame data."""Realizes a Python tag that draws an axis gizmo for its hosting object in the top left corner of the viewport at a fixed size in screen space. """ import c4d import mxutils def main() -> None: pass def draw(bd: c4d.BaseDraw) -> bool: """Draws 3D data defined in world space at a fixed size in screen space. """ # Get the global matrix of the object hosting the tag in world space. mg: c4d.Matrix = mxutils.CheckType(op.GetMain()).GetMg() # Define the world space offset as a screen space point at a given z-depth and define the axis # gizmos in world space. size: float = 100.0 offset: c4d.Vector = bd.SW(c4d.Vector(0, 0, 1000)) xAxis: c4d.Vector = offset + mg.v1 * size yAxis: c4d.Vector = offset + mg.v2 * size zAxis: c4d.Vector = offset + mg.v3 * size # Convert all vectors to screen space and then nudge them into the top left corner. delta: c4d.Vector = c4d.Vector(100, 100, 0) offset = delta - bd.WS(offset) xAxis = bd.WS(xAxis) + delta yAxis = bd.WS(yAxis) + delta zAxis = bd.WS(zAxis) + delta # Set the drawing space to screen space and draw the axis gizmos in their respective colors. bd.SetMatrix_Screen() bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_XAXIS)) bd.DrawLine2D(offset, xAxis) bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_YAXIS)) bd.DrawLine2D(offset, yAxis) bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_ZAXIS)) bd.DrawLine2D(offset, zAxis) return True
-
@ferdinand Thanks for your help.
For the unwanted effect of perspective distortion, I tried projecting a fixed size line onto an object to determine the length of the projected line in the world, and then proceeded to draw it.
code:
import c4d doc: c4d.documents.BaseDocument # The document containing this field object. op: c4d.BaseTag # The Python tag containing this code. flags: int # The execution flags of `main()`. See c4d.EXECUTIONFLAGS for details. priority: int # The execution priority of this tag. See c4d.EXECUTIONPRIORITY for details. tp: c4d.modules.thinkingparticles.TP_MasterSystem # The TP system of the document. def GetLineAndPlaneIntersection(plane_pos, plane_normal, line_start, line_dir): zero_find = line_dir.Dot(plane_normal) if zero_find == 0: return False, None else: d = (plane_pos - line_start).Dot(plane_normal) / zero_find return True, line_start + d * line_dir def main() -> None: pass def draw(bd: c4d.BaseDraw) -> bool: if op[c4d.EXPRESSION_ENABLE]: c_mg = bd.GetMg() c_mi = bd.GetMi() plane_normal = c_mg.v3 plane_100_point = c_mg * c4d.Vector(0,0,100) plane_obj_point = op.GetMain().GetMg().off intersect,intersect_100_plane_point = GetLineAndPlaneIntersection(plane_100_point, plane_normal, plane_obj_point, c_mg.off - plane_obj_point) line_end = intersect_100_plane_point + (c_mg * c4d.Vector(5,0,0) - c_mg.off) # fixed size intersect,intersect_obj_plane_point = GetLineAndPlaneIntersection(plane_obj_point, plane_normal, c_mg.off, line_end - c_mg.off) length = (intersect_obj_plane_point - plane_obj_point).GetLength() mg = op.GetMain().GetMg() pos = bd.WS(plane_obj_point) bd.SetMatrix_Screen() bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_XAXIS)) x = bd.WS(mg * c4d.Vector(length,0,0)) bd.DrawLine2D(pos,x) bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_YAXIS)) y = bd.WS(mg * c4d.Vector(0,length,0)) bd.DrawLine2D(pos,y) bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_ZAXIS)) z = bd.WS(mg * c4d.Vector(0,0,length)) bd.DrawLine2D(pos,z) return True
-
Hey,
Yes, parallel/orthographic projection is effectively just the dot product. The modelling examples cover point plane projections with a function very similar to yours. There is also c4d.utils.PointLineSegmentDistance but I never use it, as it strikes me as more cumbersome to use than just doing the math yourself.
Cheers,
Ferdinand