Entity3D: position, rotation and parent scale
Practice local/world space, −Z forward and correct normal transformation.
Run the hierarchy
Create a separate 1.0.0 C# level with an Entity3D root. Add TransformRig3D.cs and attach it. Use a 640×420 window/viewport, build and press F6. The script creates a camera, floor and parent at (1, 0, 0), rotated π/4 about Y and scaled (1.5, 1, 0.6). The blue cube is at its local point (1, 1, 0).
TransformRig3D.cs
using System;
using Reckfell;
public partial class TransformRig3D : Entity3D
{
public Entity3D Pivot { get; private set; } = null!;
public MeshInstance3D Marker { get; private set; } = null!;
public Plane GroundPlane => new(Vector3.Up, 0);
public Vector3 ProjectedPoint => GroundPlane.Project(Marker.GlobalPosition);
private static MeshInstance3D Box(Vector3 size, Color color, Vector3 position)
{
return new MeshInstance3D
{
Mesh = new BoxMesh { Size = size }, Position = position,
MaterialOverride = new StandardMaterial3D
{
AlbedoColor = color, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
}
};
}
public override void _Ready()
{
AddChild(new WorldEnvironment { Environment = new Reckfell.Environment
{
BackgroundMode = Reckfell.Environment.BGMode.Color,
BackgroundColor = new Color(0.035f, 0.04f, 0.05f)
}});
AddChild(Box(new Vector3(8, 0.05f, 6), new Color(0.10f, 0.11f, 0.13f), new Vector3(0, -0.025f, 0)));
Pivot = new Entity3D
{
Position = new Vector3(1, 0, 0), Rotation = new Vector3(0, Mathf.Pi / 4, 0),
Scale = new Vector3(1.5f, 1, 0.6f)
};
AddChild(Pivot);
Pivot.AddChild(Box(new Vector3(1.6f, 0.045f, 0.045f), new Color(0.25f, 0.46f, 1), new Vector3(0.8f, 0, 0)));
Pivot.AddChild(Box(new Vector3(0.045f, 1.5f, 0.045f), Colors.White, new Vector3(0, 0.75f, 0)));
Pivot.AddChild(Box(new Vector3(0.045f, 0.045f, 1.5f), new Color(0.45f, 0.48f, 0.55f), new Vector3(0, 0, 0.75f)));
Marker = Box(new Vector3(0.2f, 0.2f, 0.2f), new Color(0.25f, 0.46f, 1), new Vector3(1, 1, 0));
Pivot.AddChild(Marker);
AddChild(Box(new Vector3(0.25f, 0.035f, 0.25f), Colors.White, ProjectedPoint + Vector3.Up * 0.025f));
var camera = new Camera3D { Position = new Vector3(5, 4.5f, 7), Current = true, Fov = 48 };
AddChild(camera); camera.LookAt(new Vector3(0.7f, 0.4f, 0), Vector3.Up);
var canvas = new CanvasLayer(); AddChild(canvas);
bool ru = TranslationServer.GetLocale().StartsWith("ru");
var title = new Label { Position = new Vector2(24, 20), Text = ru
? "Локальная точка и мировая плоскость" : "A local point and a world-space plane" };
title.AddThemeFontSizeOverride("font_size", 22); canvas.AddChild(title);
canvas.AddChild(new Label { Position = new Vector2(24, 58), Text = ru
? "Синий куб: local (1,1,0). Белая метка: проекция на Y = 0."
: "Blue cube: local (1,1,0). White marker: projection onto Y = 0." });
ReckfellApi.Print("TRANSFORM local=" + Pivot.ToLocal(Marker.GlobalPosition));
}
public static bool TryTransformNormal(Basis basis, Vector3 normal, out Vector3 result)
{
result = Vector3.Zero;
if (!basis.IsFinite() || !normal.IsFinite() || normal.LengthSquared() < 0.000001f ||
MathF.Abs(basis.Determinant()) < 0.000001f) return false;
Vector3 transformed = basis.Inverse().Transposed() * normal;
if (!transformed.IsFinite() || transformed.LengthSquared() < 0.000001f) return false;
result = transformed.Normalized();
return true;
}
}TRANSFORM local should be near (1, 1, 0): ToLocal converts the cube's world position back into Pivot space. Parent rotation/scale changes the child's GlobalPosition without changing its Position.
Choose a space
| Property/operation | Space |
|---|---|
| Position, Rotation, Scale, Transform | Relative to the parent Entity3D |
| GlobalPosition, GlobalTransform | World |
| ToGlobal(localPoint) | This Entity3D's local point to world |
| ToLocal(worldPoint) | World point to this Entity3D's local space |
For aiming, use target.GlobalPosition − actor.GlobalPosition, or convert both points into one local space first. Vector3.Forward is (0, 0, −1). A standard −Z-facing object's world forward comes from −GlobalBasis.Column2; scale affects its magnitude, so normalize it when a direction is needed. Imported models may use another front axis: that is a model/orientation choice, not a different world convention.
Rotation without ambiguity
Rotation stores Euler radians; RotationDegrees uses degrees. Euler order affects results, and rotations around different axes do not commute. Adding three angles is not a general substitute for composing 3D rotations. Use unit Quaternions and Slerp for arbitrary orientation transitions; for aiming, check that the target differs from the position and up is not parallel to direction.
Transform3D struct methods and Entity3D methods do not always perform the same operation. Transform3D.Rotated rotates the whole transform, including Origin around the external origin; RotatedLocal rotates in the local basis and preserves Origin. Do not substitute them for an orientation edit without checking the intended pivot. See matrix composition for multiplication order.
Nonuniform scale
This example scales axes differently. Point inversion needs AffineInverse; a surface normal needs inverse-transposed Basis and renormalization. TryTransformNormal checks its chosen bounds and demonstrates that case; choose degeneracy tolerances for your task's scale.
Zero scale makes a direction noninvertible. Orthonormalized removes scale/skew rather than repairing a model for free. Rotated nonuniform-scale hierarchies may produce shear; position, Euler and Scale may not preserve that entire matrix losslessly. Separate clean rotation from scaling for complex animation and inspect the resulting hierarchy.
This is a geometry demo. It does not establish physics-collider correctness under arbitrary scale, network replication or every model-import workflow.