Transform, animate, duplicate, build a scene

Car 3D model
Car on a road
More cars!
Cars, surrounded by a wall build in code

In the last chapter, we created a TCastleScene instance. This is a very powerful class in our engine, it represents any non-trivial 3D or 2D object. You often load it's contents from the file using the Load method. To actually make it visible (and animated, and sometimes even interactive), you need to also add it to the SceneManager.Items.

In this chapter, we will extend a little the code from the previous chapter, to add more functionality around the scene.

A complete program using the code shown here is in the engine examples, in the examples/3d_rendering_and_processing/cars_demo.lpr. If you get lost or are unsure where to place some code snippet, just look there:)

1. Transform

You can group and transform (move, rotate, scale) scenes using the T3DTransform class. Let's change the program from previous chapter to make the car (one 3D object) move along a road (another 3D object).

  1. Add the Castle3D unit to your uses clause.

  2. At the place where you declared Scene: TCastleScene; in the previous chapter, change it to CarScene: TCastleScene;, and add a second scene RoadScene: TCastleScene;, and add a CarTransform: T3DTransform;.

  3. Create both scenes, placing CarScene as a child of CarTransform, and place CarTransform and RoadScene: TCastleScene; as children of SceneManager.Items.

    The complete code doing this, using TCastleWindow, looks like this:

    uses SysUtils, CastleVectors, Castle3D,
      CastleFilesUtils, CastleWindow, CastleSceneCore, CastleScene;
     
    var
      Window: TCastleWindow;
      CarScene, RoadScene: TCastleScene;
      CarTransform: T3DTransform;
    begin
      Window := TCastleWindow.Create(Application);
     
      CarScene := TCastleScene.Create(Application);
      CarScene.Load(ApplicationData('car.x3d'));
      CarScene.Spatial := [ssRendering, ssDynamicCollisions];
      CarScene.ProcessEvents := true;
     
      CarTransform := T3DTransform.Create(Application);
      CarTransform.Add(CarScene);
     
      RoadScene := TCastleScene.Create(Application);
      RoadScene.Load(ApplicationData('road.x3d'));
      RoadScene.Spatial := [ssRendering, ssDynamicCollisions];
      RoadScene.ProcessEvents := true;
     
      Window.SceneManager.Items.Add(CarTransform);
      Window.SceneManager.Items.Add(RoadScene);
      Window.SceneManager.MainScene := RoadScene;
     
      // nice camera to see the road
      Window.SceneManager.RequiredCamera.SetView(
        Vector3Single(-43.30, 27.23, -80.74),
        Vector3Single(  0.60, -0.36,   0.70),
        Vector3Single(  0.18,  0.92,   0.32)
      );
     
      Window.Open;
      Application.Run;
    end.

    If, instead of TCastleWindow, you use TCastleControl, you should be able to adjust this code easily. Move the scenes setup to TForm1.FormCreate, and declare variables as private fields of TForm1. Consult the previous chapter as needed.

    Note that we set SceneManager.MainScene as RoadScene. It doesn't really matter in this demo (and we could also leave MainScene unassigned). The MainScene determines some central things for the world (default camera, navigation mode, background / sky, fog settings). So you set MainScene to whichever 3D model determines these things for your world.

  4. To make the car actually moving, we should now update the T3DTransform.Translation property. For example, we can update it in the Window.OnUpdate callback (if you use Lazarus, there's an analogous event TCastleControl.OnUpdate).

    Before opening the window, assign:

    Window.OnUpdate := @WindowUpdate;

    At the beginning of your program (but after the definition of the CarTransform global variable), define the WindowUpdate procedure:

    procedure WindowUpdate(Container: TUIContainer);
    var
      T: TVector3Single;
    begin
      T := CarTransform.Translation;
      { Thanks to multiplying by SecondsPassed, it is a time-based operation,
        and will always move 40 units / per second along the -Z axis. }
      T := T + Vector3Single(0, 0, -40) * Container.Fps.UpdateSecondsPassed;
      { Wrap the Z position, to move in a loop }
      if T[2] < -70.0 then
        T[2] := 50.0;
      CarTransform.Translation := T;
    end;

    You will also need to add CastleUIControls to the uses clause.

That's it, you have a moving object in your world, and the movement in 100% controlled by your code!

Note: An alternative way to transform scenes is the T3DOrient class. It has the same effect, but the transformation parameters are specified a little differently — instead of a normal rotation, you specify a direction and up vector, and imagine that you transform something that has a "front" and "up" idea (like a player avatar or a creature).

You can create any complex tree this way, using the T3DTransform and T3DOrient to build any transformation hierarchy that is comfortable.

2. Play animation

Once you load a scene, you can play a "named animation" within it, using the PlayAnimation method. Open the model with view3dscene and look at the Animation -> Named Animations submenu to see what animations are available. We read animations from a variety of 2D and 3D formats (X3D, VRML, castle-anim-frames, Spine, MD3).

Car animation in view3dscene Dragon animations in view3dscene

To play the animation called wheels_turning on our sample car model, do this sometime after loading the CarScene:

CarScene.PlayAnimation('wheels_turning', paForceLooping);

You should see now that the wheels of the car are turning. Tip: If you can't easily see the wheels, remember that you can move around in the scene using mouse dragging and mouse wheel. See view3dscene documentation of the "Examine" camera mode. Of course you can configure or disable the default camera navigation in your games (see previous manual chapter for camera description).

There are many other useful methods related to "named animations". See the HasAnimation, ForceAnimationPose and AnimationDuration methods. And if these are not enough, note that the animation is just an X3D TimeSensor node. You can access the underlying node using the AnimationTimeSensor method, and use or even edit this animation as you wish.

3. Control the existence of an object

Any object descending from T3D, including TCastleScene and T3DTransform, has properties Exists, Collides and Pickable. Setting Exists to false makes the object behave like it would not be present in the SceneManager.Items tree at all — it's not visible, it's not collidable.

For example, you can toggle the visibility of the car when user presses the 'c' key, like this:

  1. Add unit CastleKeysMouse to your uses clause.

  2. Before opening the window, assign:

    Window.OnPress := @WindowPress;
  3. At the beginning of your program (but after the definition of the CarTransform global variable), define the WindowPress procedure:

    procedure WindowPress(Container: TUIContainer; const Event: TInputPressRelease);
    begin
      if Event.IsKey('c') then
        CarTransform.Exists := not CarTransform.Exists;
    end;

Advanced hints:

  • In some cases, instead of changing the Exists property, it may be easier to override the GetExists function. This is actually used by the engine to determine whether object "exists". By default it simply returns the Exists property value, but you can change it to decide existence using any algorithm you need. E.g. maybe the object doesn't exist when it's too far from the player, maybe the object "blinks" for half a second in and out.... By changing the return value of GetExists, you can make the object change it's state every frame, at absolutely zero cost.

  • Note that simply changing the SceneManager.Items contents has also almost-zero cost. So you can dynamically add and remove objects there during the game, it will be lighting fast.

  • The one thing you should not do during the game, if you hope to have a good performance: do not load from 3D model files during the game (e.g. do not call TCastleSceneCore.Load(URL, ...) method).

    If you want to add scenes dynamically during the game, it's better to load a pool of scenes at the initialization (and prepare them for rendering using the TCastleScene.PrepareResources method). Then add/remove such already-prepared scenes during the game. You can efficiently initialize many scenes from the same 3D model using the TCastleScene.Clone method, or load an X3D graph once using the Load3D function and then use repeatedly the TCastleSceneCore.Load(TX3DRootNode, ...) overloaded version.

    See the manual chapter about optimization for more hints.

4. Many instances of the same 3D object

Many car instances

It's allowed to add the same instance of the TCastleScene many times to your scene manager hierarchy. This allows to reuse it's data completely, which is great for both performance and the memory usage.

For example, let's make 20 cars moving along the road! You will need 20 instances of T3DTransform, but only a single instance of the TCastleScene. The modifications to the code are straightforward, just change CarTransform into an array:

  1. Declare it like CarTransforms: array [1..20] of T3DTransform;.

  2. Initialize it like this:

    for I := Low(CarTransforms) to High(CarTransforms) do
    begin
      CarTransforms[I] := T3DTransform.Create(Application);
      CarTransforms[I].Translation := Vector3Single(
        -6 + Random(4) * 6, 0, RandomFloatRange(-70, 50));
      CarTransforms[I].Add(CarScene);
      Window.SceneManager.Items.Add(CarTransforms[I]);
    end;

    This is the same initialization as before, we only added a randomization of the initial car position. The RandomFloatRange function is in the CastleUtils unit. There's really nothing magic about the randomization parameters, I just adjusted them experimentally to look right:)

  3. In every WindowUpdate move all the cars, like this:

    procedure WindowUpdate(Container: TUIContainer);
     
      procedure UpdateCarTransform(const CarTransform: T3DTransform);
      var
        T: TVector3Single;
      begin
        T := CarTransform.Translation;
        { Thanks to multiplying by SecondsPassed, it is a time-based operation,
          and will always move 40 units / per second along the -Z axis. }
        T := T + Vector3Single(0, 0, -40) * Container.Fps.UpdateSecondsPassed;
        { Wrap the Z position, to move in a loop }
        if T[2] < -70.0 then
          T[2] := 50.0;
        CarTransform.Translation := T;
      end;
     
    var
      I: Integer;
    begin
      for I := Low(CarTransforms) to High(CarTransforms) do
        UpdateCarTransform(CarTransforms[I]);
    end;

    This is the same code as before, just done in a loop.

Note that all 20 cars are in the same state (they display the same animation). This is the limitation of this technique. If you need the scenes to be in a different state, then you will need different TCastleScene instances. You can efficiently create them e.g. using the TCastleScene.Clone method.

5. Building and editing the scene

Cars, surrounded by a wall build in code

Up to now, we have treated TCastleScene as a kind of "black box", that can be loaded from file and then changed using only high-level methods like PlayAnimation. But you have much more flexibility.

The TCastleScene has a property RootNode that holds a scene graph of your scene. In simple cases, you can ignore it, it is automatically created when loading the model from file, and automatically changed by animations (much like the DOM tree of an HTML document changes during the page lifetime). For more advanced uses, you should know that this whole scene graph can be modified at runtime. This means that you can process the 3D models in any way you like, as often as you like, and you can even build a complete renderable 3D object by code — without the need to load it from an external file. You can also build complicated 3D objects from simple ones.

Our scene graph is composed from X3D nodes organized into a tree. X3D is a standard for 3D graphics — basically, the guys designing X3D specification have proposed a set of 3D nodes, with sensible operations, and they documented it in great detail. Our engine implements very large part of the X3D specification, we also add some extensions for cool graphic effects like shadows, shader effects and bump mapping.

Here's an example of building a scene with two simple boxes. It shows nice transparent walls around our road. It uses the RoadScene.BoundingBox value to adjust wall sizes and positions to the road 3D model.

  1. Add units X3DNodes and CastleBoxes to your uses clause.

  2. Define a function that builds a scene from X3D nodes:

    function CreateBoxesScene: TCastleScene;
    const
      WallHeight = 5;
    var
      RoadBox: TBox3D;
      RootNode: TX3DRootNode;
      Appearance: TAppearanceNode;
      Material: TMaterialNode;
      Shape1, Shape2: TShapeNode;
      Box1, Box2: TBoxNode;
      Transform1, Transform2: TTransformNode;
    begin
      { The created geometry will automatically adjust to the bounding box
        of the road 3D model. }
      RoadBox := RoadScene.BoundingBox;
      if RoadBox.IsEmpty then
        raise Exception.Create('Invalid road 3D model: empty bounding box');
     
      Material := TMaterialNode.Create;
      { Yellow (we could have also used YellowRGB constant from CastleColors unit) }
      Material.DiffuseColor := Vector3Single(1, 1, 0);
      Material.Transparency := 0.75;
     
      Appearance := TAppearanceNode.Create;
      Appearance.Material := Material;
     
      Box1 := TBoxNode.Create('box_1_geometry');
      Box1.Size := Vector3Single(0.5, WallHeight, RoadBox.Sizes[2]);
     
      Shape1 := TShapeNode.Create('box_1_shape');
      Shape1.Appearance := Appearance;
      Shape1.Geometry := Box1;
     
      Transform1 := TTransformNode.Create('box_1_transform');
      Transform1.Translation := Vector3Single(RoadBox.Data[0][0], WallHeight / 2, RoadBox.Middle[2]);
      Transform1.FdChildren.Add(Shape1);
     
      Box2 := TBoxNode.Create('box_2_geometry');
      Box2.Size := Vector3Single(0.5, WallHeight, RoadBox.Sizes[2]);
     
      Shape2 := TShapeNode.Create('box_2_shape');
      { Reuse the same Appearance node for another shape.
        This is perfectly allowed (the X3D is actually a graph, not a tree). }
      Shape2.Appearance := Appearance;
      Shape2.Geometry := Box2;
     
      Transform2 := TTransformNode.Create('box_2_transform');
      Transform2.Translation := Vector3Single(RoadBox.Data[1][0], WallHeight / 2, RoadBox.Middle[2]);
      Transform2.FdChildren.Add(Shape2);
     
      RootNode := TX3DRootNode.Create;
      RootNode.FdChildren.Add(Transform1);
      RootNode.FdChildren.Add(Transform2);
     
      Result := TCastleScene.Create(Application);
      Result.Load(RootNode, true);
    end;

    Note that to transform X3D nodes we use the TTransformNode class. We have essentially two transformation trees in our engine:

    1. The "outer" tree is rooted in SceneManager.Items, and shows scenes TCastleScene transformed by T3DTransform and friends.
    2. The "inner" tree is inside every scene. It is rooted in TCastleSceneCore.RootNode, and shows shapes TShapeNode, and is transformed by TTransformNode.

    This is explained in detail in the chapter about the transformation hierarchy.

  3. Add the created scene to the scene manager, by adding this somewhere at the end of scene manager initialization:

    Window.SceneManager.Items.Add(CreateBoxesScene);

Note: in this case, it would probably be simpler to add these 2 walls (boxes) to the road.x3d file in Blender. Thus, you would not need to deal with them in code. But in general, this technique is extremely powerful to generate 3D scenes following any algorithm!

To construct a more flexible mesh than just a box, you can use a universal and powerful IndexedFaceSet node instead of a simple Box. For IndexedFaceSet, you explicitly specify the positions of the vertexes, and how they connect to form faces.

See the examples:

  • examples/3d_rendering_processing/build_3d_object_by_code.lpr (rather simple example)
  • examples/3d_rendering_processing/build_3d_tunnel.lpr (a cool example generating a tunnel mesh).

6. Further reading

6.1. Scene Manager: advanced notes

  • You can use many scene manager instances. Just create your own TCastleSceneManager instance. You can use TCastleWindowCustom to get a window that does not have the default scene manager created for you — sometimes it's easier to dynamically create, add and remove as many scene managers as you want.

    You can display multiple scene managers, each showing different set of objects, on top of each other. This way you can easily divide your world into "layers". The scene managers order is determined by their 2D order (you insert them using methods like InsertFront, InsertBack). This way you explicitly render some objects on top of other objects, regardless of their positions in a 3D world.

    When using multiple scene managers on top of each other, remember that the TCastleSceneManager by default renders a background covering everything underneath. You can disable this background by setting SceneManager.Transparent to false. The 2D scene manager, in T2DSceneManager, has already Transparent = true by default.

  • You can also make alternative views into the same world (same scene manager). For this, use TCastleViewport , that points to a TCastleSceneManager for information about the world (but has it's own camera).

    For examples of this, see:

    • The chapter about user interface shows how to set additional viewport.
    • Engine example examples/3d_rendering_processing/multiple_viewports.lpr
    • Engine example examples/fps_game/fps_game.lpr

6.2. Creating descendants, implementing AI

You can create descendants to customize all the classes mentioned here. These descendants can override methods e.g. to collide or perform AI (move itself in the world).

Every object (a descendant of T3D, like TCastleScene or T3DTransform) "knows" it's World so it knows how to move and collide within the 3D world. This opens various ways how you can implement "artificial intelligence" of a creature, for example:

  1. Derive your creature class from a T3DTransform or T3DOrient.
  2. Override it's Update method to move the creature. Use T3D.Move, T3D.MoveAllowed, T3D.Height and T3D.LineOfSight methods to query the world around you.
  3. As a child of your creature instance, add a TCastleScene that shows an animated creature.

In fact, this is how our creatures with ready AI are implemented:) But you can do it yourself.

6.3. It all applies to 2D too!

Basically, you don't need to learn anything new for 2D games. You can load 2D models (from X3D, VRML, Spine or any other format) in TCastleScene, and process them with TCastleSceneManager.

Do not be discouraged by the names of some classes starting with T3D... prefix, like T3DTransform. These classes can deal with 3D as well as 2D objects, as 2D in our engine is just a special case of 3D. Just use T3DTransform to transform your 2D scenes, it works perfectly. (At some point, we will migrate to better names...)

Often it's also a good idea to use a specialized 2D classes when they exist:

  • T2DScene: descendant of TCastleScene that is especially suitable for 2D scenes.

    It has a little better default Z-sorting settings, so that blending "just works".

  • T2DSceneManager: descendant of TCastleSceneManager especially suitable for 2D world.

    It has a little more comfortable default camera and projection settings for 2D. Also, it has by default SceneManager.Transparent = true, so you can see the background underneath (although you can change it to false if you want of course). This way, it can be easily used as a 2D user-interface control, to show something animating over a GUI.