X3d geometry components, docs outdated?

Hello again.

I’d like to draw a flat (2D) polygon with an outline (thick and different color) and some added details.
Then i’d like to move (a la TransformXY) and rotate the shape. Maybe also to scale/zoom it.

It seems rather straightforward using DrawPrimitive2D with GL TrinaglesFan and GL Lines mode.
But i’d like to move to “declarative approach” instead.

Obvious starting points seems to be:

The source examples there are inspiring, but sadly they do not give conceptual overview, what steps / added info is required to turn array of points/vertexes into a figure. There should be some colours, stencils, etc…

  1. Supported nodes …

So far so good. X3D ref seems to say there would be no such thing as “fan” built around some natural “center”, but it would not be hard to copy the initial vertex. Except that…

type TTriangleSet2DNode = class(TAbstractGeometryNode)
Set of triangles in 2D. Rendering of this node is not implemented yet.

Ooops… TO me this sounds weird, in which sence that component is “supported” if it is not rendered? Or is it?

function TTriangleSet2DNode.Proxy seems implemented and on a cursory look it does not seem much different from TRectangle2DNode.Proxy, as for the rest it seems mostly auto-generated.

If it still is NOT rendered, then what do i miss?

But yahoo, the sources teach me about TTriangleSetNode and TTriangleFanSetNode. The last sounds very good… Yet those are not even mentioned at Geometry3D component | Castle Game Engine

Are them also non-implemented about rendering? But there is no such warning in docs and sources it seems. Castle Game Engine: X3DNodes: Class TTriangleFanSetNode

Also, i wonder if that is possible to translate all 3D classes to their 2D counterparts. It seems it can be well templates, it seems all one needs is to replace Vector3 with Vector2 as coordinates type and add explicit property Z. Perhaps it would miss a goal of straightforward mapping of XML nodes, though. Yet maybe some easy conversion of 2D to 3D, like https://www.makewordart.com could be handy.

  Transform := TTransformNode.Create;
  RectGeometry := TRectangle2DNode.CreateWithShape(RectShape);

But why?
constructor CreateWithTransform(out Shape: TShapeNode; out Transform: TTransformNode);

  1. Aside from Geometry3D component | Castle Game Engine , Geometry2D component | Castle Game Engine , see also “Building a mesh using code” on Writing code to modify scenes and transformations | Manual | Castle Game Engine .

  2. As for “Rendering of this node is not implemented yet.” at TTriangleSet2DNode docs – ups, that’s just a mistake :slight_smile: Removed it.

    Note that all 2D nodes are actually rendered using their 3D counterparts, as in practice there’s no noticeable overhead in doing so, and it is easier. Moreover, the 3D nodes are generally more feature-rich. So in practice, you’re fine just using 3D nodes, like TTriangleSetNode, for 2D too – just set the Z to 0 everywhere, that’s it, there’s nothing special about “2D” aside from this :slight_smile:

  3. TTriangleSetNode and TTriangleFanSetNode are part of Rendering component, Rendering component | Castle Game Engine , not Geometry3D.

    And they are both implemented (as they are documented inside “Supported Nodes” on that page).

    Personally I think they should be part of Geometry3D, the current distinction between Rendering (“stuff that doesn’t need triangulation”) and Geometry3D (“stuff that needs triangulation”) is probably more confusing than helpful. But then, this is the division in X3D, I didn’t want to introduce more confusion by merging them and being inconsistent with how X3D describes them.

  4. As for “Also, i wonder if that is possible to translate all 3D classes to their 2D counterparts.” – it is possible, but a bit pointless. I mean, the gain (you can build 2D primitives without specifing Z = 0 everywhere) is less that the additional complication (much more nodes, and their handling introduces additional conversion step). We already have TTriangleSet2DNode that is analogous to TTriangleSetNode… and the usefullness of it is rather low. TTriangleSetNode works as nicely.

As for “why” – all geometry nodes have additional constructors “CreateWithShape” and “CreateWithTransform” (which is really “create with both shape and transform”). It’s just a useful shortcut – when creating a geometry node, you almost always want to also create a shape node, and then usually also a transform node.

Thanks for hints. Here is a quick reply, i did not have time to look into “Building a mesh using code” yet but again thanks for the pointer!

  1. As for “why” – all geometry nodes have additional constructors

Ermmm… you understood me the opposite way. I mean, why you even do that TTransformNode.Create; explicitly when you have the seemingly fitting helper constructor?

Either create both Shape and Tranform explicitly and outside - or create both by the helper constructor.

Is it just a dusty old code from times when you had no CreateWithTransform yet, or am i missing something and it can not be the substitute?

Also - while it is a bit stupid for a German and Russian guys to reflect upon details of English - i still think CreateWithXXXX was a bad name. I dived into the code why? because i noticed that you fail to initialize the Shape, and then you pass that shape as an input-parameter into constructor…

Yet again, not being native speaker, but “Whiskey with cola” d not sound to me as whiskey creating cola, but that pre-made cola was mixed into whiskey.

I see you tried to rename it, but it did not made things much better, not for me at least. It still looked like non-initialized in-param until i dived into the sources.

Frankly, i do not know if there can be concise yet specific and nice way to name those functions. CreateGivingXXX, CreateMeAndCreateXXX, CreateProvidingXXXX, CreateCreatingXXX - those all feel ugly to me. I failed to imagine any good naming…

That said, personally i would just not have any different names at all.

type PShapeNode = ^TShapeNode; PTransformNode = ^TTransformNode;

private constructor Create(const SH: PShapeNode; const T: PTransformNode ); overload;

   constructor Create(out SH: TShapeNode; out T: TTransformNode ); overload;
   constructor Create(out T: TTransformNode; out SH: TShapeNode ); overload;
   constructor Create(out SH: TShapeNode); overload;
   constructor Create(out T: TTransformNode); overload;
   constructor Create(); overload;

with the obvious bridging to the real (ptr, ptr) constructor.

 constructor xxxx.Create(out T: TTransformNode); overload;
    Create( nil, @T );

Since i can’t come out with a decent name - then why bother at all? :smiley:

mean, the gain (you can build 2D primitives without specifing Z = 0 everywhere) is less that the additional complication (much more nodes, and their handling introduces additional conversion step).

Yeah, that is the question, and i am on the fence about it.

But then, maybe the best of both worlds can be done by adding a special method? like you have TransformXY augmenting full-featured Transform, like the RotationAngle i proposed.

Right now you have in your 3D classes the method OutlineCoords.SetPoint( const Vs: array of Vector3 );
Perhaps those very 3D classes could be augmented with method like OutlineCoords.SetPointXY( const Vs: array of Vector2; const Z = 0.0 ); ?

Personally I think they should be part of Geometry3D, the current distinction between Rendering (“stuff that doesn’t need triangulation”) and Geometry3D (“stuff that needs triangulation”) is probably more confusing than helpful.

see also “Building a mesh using code”

Yeah, this “look from above” on the whole picture seems really missing from the docs. It is in your head, but is not easily accessible to bystanders :frowning:

That’s why i speculated the doc is shabby and obsolete on that part.

In particular, the Geometry2D demo seems to me as an attempt to ride two horses, to show explicit creation of TTransformNode yet at the smame time to ignore it and keep things as simple as possible.

If i get the gist behind embedding the TTransformNode then perhps it would be better to have two demos. One would do whole rotation and would not have any explicit TTransformNode at all, like it was suggested in the comments.

  // Note: in this case, since you just rotate whole Scene,
  // you could also rotate it like this:
  //Scene.Rotation := Vector4(0, 0, 1, LifeTime * 2);
  // There's no need for TTransformNode in this case.

See, trying to squeeze opposite intentions into one…

Just make a demo#2 on top of the first, having some segmented shape, like railway semaphore, or a clock with hour and minute arrows/hands, or a torso with limbs. Then you create TTransformNode to represent moving joints, right? And so it would be obvious intention and implementation, on top of the basic “create simplistic anything” as demo#1

I simply didn’t always use the CreateWithShape or CreateWithTransform shortcut, to show it is possible to work without them.

Nothing dusty or whatever.

As for naming – well these are the best names I came up with. Overloading Create would be much more confusing, so no.

Maybe. The usefulness of this shortcut would still be limited – how hard it is to just pass an array of TVector3? So not for now, but I’ll keep it in mind.

This is a good idea. Though note that ideally, I want to get rid of 2 alternative ways to transform stuff in CGE – we have TTransformNode and TCastleTransform and there are many situations when both are sensible solutions. In the long run, these should be one and the same, Roadmap | Castle Game Engine .

Still, surely more demos showing it would be nice, good point.

i guess the result is the opposite, you imply there is one hidden differnce there, why one can be used and another can not. The novice reader is left confused (F.U.D.) as what fine reasoning did he missed…

i guess it would be better to use the helper and then add the comments that it is equivalent with this explicit and verbose code.

Not sure.

CGE is not just a thin wrapper over x3d, so it can deviate.

To me TCastleTransform is “programming by contract” and TTransformNode is “implementation detail”. It might be reasonable to make every CT refer to explicit or implicit TN behind the cover. But still i think making them one and the same would be leaky abstraction and tight coupling

That is a good point, and I also view them this way.

I mean, in general, in most CGE API, we try to hide X3D nodes, and give you higher-level things to work with. Like our TCastleBox or TCastlePointLight, that underneath wrap some X3D nodes, but you even cannot deal with these X3D nodes directly. Or TCastleScene, that is really a big wrapper over TX3DRootNode – and you can, but in 90% cases you don’t have to deal with X3D nodes inside MyScene.RootNode.

This makes better API – X3D is sometimes too low-level, and/or sometimes it has it’s “weirdness” that we prefer to hide.

So I don’t really want to “burden” TCastleTransform API with some additional X3D-specific stuff. Indeed, the link TCastleTransform<->TTransformNode should be “under the covers”.

if the app/game is generally 3D in it’s internal data - then there probably is some ready-made array already, indeed.

if the app is mostly 2D inside - then there would be tedious conversions here and there.

and exactly because they would be simple and boring developer would have his guards down and would start overlooking copy-paste typos. That is why utility functions are made, to write it once and only use later :slight_smile:

Which perhaps means there’d be a global utility
function Vector3s( const Vs: array of TVector2; const Z: ... = 0.0 ): TArray<TVector3>; overload;
and the said methods perhaps should just call it.

Just some night musing…

Interesting, that you kind of already have it, albeit reserved in ownership.

I found this function TVector3List.ToVector4(const W: Single): TVector4List;

Strangely, there is no similar helpers in TVector3List. It was more of ad hoc hack than a consistent design choice. But if so then WHY was it promoted into a class member?..

Yeah, yeah, List is not an array, so this function is dangerous of memory leaks, and classes are not a fastest thing either (some speed-obsessed libs even stuck with objects instead of classes, at least until “advanced records” came). But still, that function is there. Alone.

Also, after wetting my feet in functional programming, i’d more appreciate “pull” than “push”.
IOW i’d more like - assuming all other things the same - “pure function” way of constructor ToVector4List.Create(const TVector3List or TArray<TVector3>) than a pattern like function TStringList.ToStringArray();

And then we hypothetically could have (abuse?) implicit typecasts, which some Scala folk christened a “magnet pattern” and which they used to fight combinatorial explosion in overloaded methods.

Funny that some years before them coining it i tried to do a similar thing to char and string functions in Jedi Code Library, which names and arguments were kinda erratic. This did not wanted by upstream uch and was not completed, though. Because - jsut a geist and synthetic example - a hypothetical function to subtract a set of char from another could have each of their arguments as a char, an array of, a string of, or a set of, or even a TList< char >. “Classic” way could be to provide a function (maybe an overload, otherwise an extra problem of reasonable names comes) for every combination of argument types, which is the ad and fragile explosion. Alternative way would be to make one funciton with some selected final type of arguments, and then to provide the compiler a set of implicit type converting functions, which it would apply at call sites.

Granted, Delphi and FPC are not very good with “type inference” and thus with implicit typecasts too…

Now, why you use procedure SetPoint(const Value: array of TVector3); overload; instead of procedure SetPoint(const Value: TArray<TVector3>); overload; in places like TCoordinateNode ?

Just legacy and avoiding boilerplate of creating named dynarray types in pre-generics FPC?

Or do open arrays (TWO parameters with no refcounting) actually work better and faster for this application than a dynamic array (single parameter but might trigger refcounting later in the flow) ?

I mean, the obvious TCoordinateNode.SetPoint( TVector3List.CreateOutOf( array of Vector2, Z ) ) would leak and thus is non-starter.

However a compiler-managed type, like dynarray, could pursue this way, hypothetically.

Like TVector3 is not just an array but a record containing array.
So could there be a record TVector3Array packing in extra methods, including implicit and explicit type casts. And then a hypothetical TCoordinateNode.SetPoint( TVector3Array.Create( array of Vector2, Z ) ) could fly.

Even if that hypothetical TVector3Array was an array nor record (like ad hoc TBytes and TStringDynArray in Delphi 7) then maybe and with some compilers auto-casting could be provided as record helper/type helper. Those helpers seem to me too overused by Delphi XE3+ RTL - basically abusing the concept to gloss over ocmpiler shortcomings - but that train has already departed whether i like it or not.

Or, returning to the comment start, there could be function TVector2List.ToVector2(const Z: Single = 0): TVector3Array and function TVector2Array.ToVector3(const Z: Single = 0): TVector3Array, providing for a better readable call site like TCoordinateNode.SetPoint( my_2d_array.ToVector3(100) ).

Getting effectively the same without adding an yet-another-name function like SetPointXY nor a 3rd overload of SetPoint.

Basically, type conversion really feels a generic enough task that should not be embedded inside data consumers, which would result in ugly copy-pasting those helper functions all across the records and classes. All those “pseudo-generic” containers libraries of Delphi 7, be them macros-based or copy-paste-based, were really itchy to see.

In general we encourage to use classes (list TVector3List) for containers, instead of (dynamic) arrays, because the classes are more packed with features and can be extended with even more. (This got a bit blurry when type helpers for dynamic arrays became possible, but still I feel classes are just a “complete solution” to containers).

Some API accepts both a class and an array (like above SetPoint) because I recognize that sometimes an array is still simpler to use.

The conversion routines like TVector3List.ToVector4 are indeed just added when they proved useful / necessary. I do not want to commit to expose every possible combination there – it is too easy to make a big API with lots of such “utility” methods that will be almost unused then.

Thinking about 2D coords more, I realized that what we really need, and this would be more comfortable and also more performant in some cases, is a new TCoordinate2DNode. This one should keep a list of TVector2. It’s better than overloading 3D TCoordinateNode with utility functions to load there 2D data, because it means that TCoordinate2DNode can be loaded to GPU as 2D, so it will be faster to send to GPU. Following the conventions (from OpenGL, CGE) etc. of course such 2D vector is functionally equivalent to 3D vector with Z = 0. Just like 3D vector is functionally equivalent to 4D vector in homogeneous coordinates with W = 0.

Still, I’m not saying when I’d add it. The argument “this has low usage” remains. Neither glTF (POSITION must be VEC3, glTF™ 2.0 Specification ) nor X3D (the only X3DCoordinateNode descendants are 3D, Extensible 3D (X3D), ISO/IEC 19775-1:2022, 11 Rendering component ) have added such 2D coords, even though the opportunity is obvious (and it would also imply smaller file size), likely exactly for this reason – sure it can be added, but if the real-world usage is low, then the priority to do this is low.

This is one specific OpenGL “implicit type conversion” trick. It won’t work if Z <> 0 or if orthogonal rotation applied (i mean, 2D are X and Z or Y and Z, not X and Y).

Not sure if such a narrow special case is required. What if there is 3D rotation transformation applied to that X-Y-zero plane? Would you still have to calculate all 3 coords via rotation matirx, or would you tell OpenGL to do the turn?

Then how would it go with collision detection? I am not sure if rotation offloading to OpenGL is a good thing. I thought i heard “Scene definition” subpart of OpenGL was considered obsolete together with outher fixed-function pipeline related functions. But not sure. Yet even if it is normal, later you would have to re-calculate that again in CPU to check collisions?

These do not claim rotation as obsolete, so perhaps i am wrong. Still not sure if you use them.

Also, you kind of miss the point. I was not asking class vs array, i was asking dynamic array vs open array.

You remember that procedure X( ns: array of integer ); technicalyl has TWO parameters, not one, don’t you?

Yes, this syntax overlapping is a confusing legacy of pre-dynarray Turbo Pascal, and in “Modern Pascal” i’d expect procedures to be changed to use “generic dynamic array” instead of “open array”, unless low-level “this is two parameters not one” is exactly what you aim at (then it better be documented).

a bit blurry when type helpers for dynamic arrays became possible

Type helpers were not designed to uniformly extend types, but to hack on 3rd party libraries you have no source for (or wish to keep it vanilla). “Retconning” as they say on TV Tropes :slight_smile:

Delphi XE3 decided to (ab)use type helpers to gloss over lack of type inference in the compiler, or so i think.

To get “best of both worlds” i believe one has to use advanced records, which naturally blend data and funcitons into one local namespace, while type helpers are global and mutually exclusive (try to have two helpers for one type).

I suspect record-boxed dynarray would be somewhat slower than naked dynarray, yet still faster than Delphi-like class instance.

As for TCoordinate2DNode:

  • Indeed it would limit the case to situation where Z = 0.

  • But of course you can transform such objects. You can move them (also in Z), rotate them, scale etc. It’s normal and its what regularly happens in 2D games :slight_smile:

  • Fixed-function, that offered one way of transforming objects (glRotatef and friends) is deprecated. But of course you still can, and everyone does it, transform (rotate, translate, scale…) objects in OpenGL, using shaders (most commonly: using matrix multiplication in vertex shader). Nothing special about it, it’s what happens already in CGE and pretty much all game engines :slight_smile:

    Yes, in case of collisions you also have to do it on CPU (or whatever your physics engine uses under the hood). It doesn’t mean you have to transform each vertex, common solution is to transform the “queried object” (like ray) into local shape coordinates. And for collisions, you also often use simplified shapes.

    So, nothing special here, transforming objects is normal :slight_smile:

  • The reason why I wonder (but still only wonder) about TCoordinate2DNode is that it not only addresses a common case in 2D (specifying object with constant Z = 0) but also can be more efficient. Sending a million of points in TCoordinate2DNode means 2/3 less data transfer to GPU vs million of points in TCoordinateNode.

    So this would bump the usefulness of TCoordinate2DNode in my eyes. It not only provides a “shortcut” (that could be alternatively done at various levels in Pascal API), it also provides a very natural optimization that is otherwise not (easily) possible. (While the engine could try to detect special cases of TCoordinateNode with all Z = 0, and send it as 2D data to OpenGL, but this would more than defeat the gain from the optimization.)