Grouping component

This component defines the basic nodes to operate on groups of other nodes. Transform node allows to additionally translate, rotate or scale a group of nodes. Switch node allows to choose one children from a group of nodes (which is a useful tool for various interactive animations).

See also X3D specification of the Grouping component.

Contents:

1. Supported nodes

  • Group(Pascal API: TGroupNode)

    Group of other X3D nodes.

  • Transform(Pascal API: TTransformNode)

    Group of other X3D nodes, that additionally transforms (translates, rotates, scales) it's children. This is the most basic X3D node to create a transformation hierarchy. Build a hierarchy of Transform nodes (one Transform node may be child of another, of course), and place Shape nodes inside, and you have a transformation hierarchy. You can use interpolation nodes to animate it.

    Note that a Transform node that does not perform any transformation (leaves translation, rotation, scale at default values) is equivalent to the Group node. If you don't plan to ever modify the transformation of children, it's a little more efficient to use the Group node.

  • Switch(Pascal API: TSwitchNode)

    Group of other X3D nodes, of which only one (or none) is active at a given time. "Active" means that a child (including all child's children) can be visible and colliding. The field whichChoice specifies the index of the active child, negative value like -1 means that no child is active.

    In a way, this node is like case in Pascal or switch in C-like languages: only one (or none) child is active.

    Switch can be used to choose one node from many, e.g. a user selects something and you can display either a sword, or a shield, or a chest.

    Switch can also be used to toggle the existence of a single node. In this case, you use Switch node with only one node in the children list, and you set whichChoice to 0 (show the child) or -1 (don't show the child). Note that to easily toggle the visibility of the shape you can alternatively use boolean Shape.render field.

  • StaticGroup(Pascal API: TStaticGroupNode)

    Group of nodes that is guaranteed to never change at runtime. This node is similar to the Group node, but it can never change at runtime (e.g. due to X3D events or Pascal code). This way it may work faster. It is up to the developer to avoid changing the node contents, it is undefined what will happen if a change occurs (maybe the change will be ignored, maybe it will cause an error).

    Currently CGE doesn't use the possible optimizations offered by this node. It is treated exactly like Group. If you have a Group of nodes and you will never change it at runtime, we are actually already quite efficient at it. Future version may bring e.g. "static batching" that could look at this node.

Note: The bboxCenter and bboxSize fields of X3D grouping nodes are right now ignored by CGE. Instead, we internally always calculate and update best bounding boxes (and bounding spheres) for collision. So there's no need to fill these X3D fields.

2. How to calculate bounding box of something

There are various properties in CGE that may specify the bounding box of something. Some of them are calculated by our engine, and updated (e.g. on animations) and are guaranteed to contain a useful value. Some of them do not — they may contain a value, useful for some optimizations, but it is not guaranteed.

To summarize:

  • To have a reliable bounding box of a scene (actually, any transform), that is calculated by CGE, and updated (e.g. in case of animations) by CGE, you should use one of these:

    Use these to calculate bounding box of the entire scene (and all children scenes). See their docs for details about their coordinate systems.

  • To have a reliable bounding box of a shape, that is calculated and updated (e.g. in case of animations) by CGE, you should use one of these:

    • TShape.BoundingBox. It is specified in the scene coordinates (transform by MyScene.WorldTransform to get the bounding box in world coordinates).

    • TShape.LocalBoundingBox. It is specified in the shape coordinates. Transform it by MyScene.WorldTransform * MyShape.OriginalState.Transformation.Transform to get the bounding box in world coordinates.

    To find the TShape instance you are interested in, find it among the Scene.Shapes. For example search it using the MyScene.Shapes.Traverse or MyScene.Shapes.TraverseList.

    TShape has a lot of information to inspect what it is. E.g. TShape.Node, that points to TShapeNode, which name you can in turn check e.g. like MyShape.Node.X3DName. We also store information about the parent nodes of shape, in TShape.GeometryParentNode, TShape.GeometryGrandParentNode, TShape.GeometryGrandGrandParentNode. You can look at shape geometry in TShape.Geometry.

  • The TAbstractGroupingNode.BBox may specify the bounding box of a group of nodes.

    This property has the value that was specified in the X3D file. Or a value that was manually set there by some Pascal code (e.g. when converting something to X3D nodes). But no code in CGE does this right now.

    If provided, this value is given in local group coordinates.

    CGE does not validate, or calculate, or ever update this value. It is also not used by CGE for anything, right now (but we could use it for optimizations in the future).

    If the X3D file didn't specify any value, it will be an empty bounding box. You can check it by TBox3D.IsEmpty.

    Note: the BBox underneath uses FdBBoxCenter, FdBBoxSize. We discourage from using these lower-level fields directly, and they don't provide any extra information.

  • The TAbstractShapeNode.BBox may specify the bounding box of a given shape.

    This property has the value that was specified in the X3D file. Or a value that was manually set there by some Pascal code (e.g. when importing glTF to X3D nodes). glTF importer right now sets this value.

    If provided, this value is given in local shape coordinates.

    CGE does not validate, or calculate, or ever update this value. But, if it is specified (provided in X3D file or by some Pascal import code) then we use it, for optimization.

    If the X3D file didn't specify any value, it will be an empty bounding box. You can check it by TBox3D.IsEmpty.

    Note: the BBox underneath uses FdBBoxCenter, FdBBoxSize. We discourage from using these lower-level fields directly, and they don't provide any extra information.

    In summary:

3. TODO

Below fields are not implemented yet, but certainly planned and should be easy. Please report if you need any of these high-priority.

  • X3DGroupingNode.visible (Note: we do support X3DShapeNode.visible, so you can hide individual shapes.)
  • X3DGroupingNode.bboxDisplay

4. Example in Pascal

Demo of grouping X3D nodes

The example below builds a node hierarchy like this:

Group (root node)
-> Group
   -> Transform (translate x = 1)
      -> Shape (red material)
         -> Box
   -> Transform (translate x = 2)
      -> Shape (green material)
         -> Box
   -> Transform (translate x = 3)
      -> Shape (blue material)
         -> Box
-> Transform (translate to the right and down)
   -> Switch
      -> Shape
         -> Sphere
      -> Shape
         -> Cone
      -> Shape
         -> Cylinder

The Group and Transform simply arrange the nodes positions on the screen.

Note that this node hierarchy could be encoded in X3D (in XML or classic encoding) as well, and only loaded from Pascal. This has some benefits (e.g. an X3D file can be tested by view3dscene). Below we construct everything in Pascal just as a demo, to show that it is possible.

Press the s key to toggle what is displayed in the Switch node: one of the children, or nothing.

{ Demo of Group, Transform, Switch nodes. }
 
uses SysUtils,
  CastleWindow, CastleScene, CastleViewport, CastleCameras,
  CastleColors, CastleVectors, CastleFilesUtils, X3DNodes, CastleKeysMouse;
 
var
  Switch: TSwitchNode;
 
function BuildRootNode: TX3DRootNode;
var
  GroupBoxes: TGroupNode;
  Box: TBoxNode;
  BoxTransform: TTransformNode;
  BoxShape: TShapeNode;
  BoxMaterial: TMaterialNode;
  BoxAppearance: TAppearanceNode;
  TransformSwitch: TTransformNode;
  SwitchChildShape: TShapeNode;
  SphereAppearance: TAppearanceNode;
begin
  Result := TX3DRootNode.Create;
 
  GroupBoxes := TGroupNode.Create;
  Result.AddChildren(GroupBoxes);
 
  { create red box }
 
  Box := TBoxNode.CreateWithTransform(BoxShape, BoxTransform);
  Box.Size := Vector3(0.75, 0.75, 0.75);
 
  { TBoxNode.CreateWithTransform is a shortcut for:
 
    Box := TBoxNode.Create;
    BoxShape := TShapeNode.Create;
    BoxShape.Geometry := Box;
    BoxTransform := TTransformNode.Create;
    BoxTransform.AddChildren(BoxShape);
  }
 
  BoxTransform.Translation := Vector3(1, 0, 0);
 
  BoxMaterial := TMaterialNode.Create;
  BoxMaterial.DiffuseColor := RedRGB;
 
  BoxAppearance := TAppearanceNode.Create;
  BoxAppearance.Material := BoxMaterial;
  BoxShape.Appearance := BoxAppearance;
 
  GroupBoxes.AddChildren(BoxTransform);
 
  { create green box }
 
  Box := TBoxNode.CreateWithTransform(BoxShape, BoxTransform);
  Box.Size := Vector3(0.75, 0.75, 0.75);
  BoxTransform.Translation := Vector3(2, 0, 0);
  BoxMaterial := TMaterialNode.Create;
  BoxMaterial.DiffuseColor := GreenRGB;
  BoxAppearance := TAppearanceNode.Create;
  BoxAppearance.Material := BoxMaterial;
  BoxShape.Appearance := BoxAppearance;
  GroupBoxes.AddChildren(BoxTransform);
 
  { create blue box }
 
  Box := TBoxNode.CreateWithTransform(BoxShape, BoxTransform);
  Box.Size := Vector3(0.75, 0.75, 0.75);
  BoxTransform.Translation := Vector3(3, 0, 0);
  BoxMaterial := TMaterialNode.Create;
  BoxMaterial.DiffuseColor := BlueRGB;
  BoxAppearance := TAppearanceNode.Create;
  BoxAppearance.Material := BoxMaterial;
  BoxShape.Appearance := BoxAppearance;
  GroupBoxes.AddChildren(BoxTransform);
 
  { create translated Switch node with children }
 
  TransformSwitch := TTransformNode.Create;
  TransformSwitch.Translation := Vector3(2, -2, 0);
  Result.AddChildren(TransformSwitch);
 
  Switch := TSwitchNode.Create;
  Switch.WhichChoice := 0; // initially
  TransformSwitch.AddChildren(Switch);
 
  TSphereNode.CreateWithShape(SwitchChildShape);
  SphereAppearance := TAppearanceNode.Create;
  SphereAppearance.Material := TMaterialNode.Create; // assign any material, to make it lit
  SwitchChildShape.Appearance := SphereAppearance;
  Switch.AddChildren(SwitchChildShape);
 
  TConeNode.CreateWithShape(SwitchChildShape);
  SphereAppearance := TAppearanceNode.Create;
  SphereAppearance.Material := TMaterialNode.Create; // assign any material, to make it lit
  SwitchChildShape.Appearance := SphereAppearance;
  Switch.AddChildren(SwitchChildShape);
 
  TCylinderNode.CreateWithShape(SwitchChildShape);
  SphereAppearance := TAppearanceNode.Create;
  SphereAppearance.Material := TMaterialNode.Create; // assign any material, to make it lit
  SwitchChildShape.Appearance := SphereAppearance;
  Switch.AddChildren(SwitchChildShape);
end;
 
procedure WindowPress(Container: TUIContainer; const Event: TInputPressRelease);
begin
  if Event.IsKey(keyS) then
  begin
    // Switch.WhichChoice cycles from -1 to 2.
    Switch.WhichChoice := Switch.WhichChoice + 1;
    if Switch.WhichChoice > 2 then
      Switch.WhichChoice := -1;
  end;
end;
 
var
  Window: TCastleWindow;
  Viewport: TCastleViewport;
  Scene: TCastleScene;
begin
  Window := TCastleWindow.Create(Application);
  Window.Open;
  Window.OnPress := @WindowPress;
 
  Viewport := TCastleViewport.Create(Application);
  Viewport.FullSize := true;
  Viewport.Camera.Translation := Vector3(2, -1, 8);
  Window.Controls.InsertFront(Viewport);
 
  Scene := TCastleScene.Create(Application);
  Scene.Load(BuildRootNode, true);
  Scene.PreciseCollisions := true;
 
  Viewport.Items.Add(Scene);
 
  Viewport.InsertFront(TCastleExamineNavigation.Create(Application));
 
  // add a simple headlight, i.e. directional light attached to the camera
  Viewport.Camera.Add(TCastleDirectionalLight.Create(Application));
 
  Application.Run;
end.