Плоскости: сторона, расстояние и проекция
Нормализованное уравнение плоскости, пересечение луча/отрезка и проекция мировой точки.
Что означает Plane
Плоскость задаётся уравнением Normal.Dot(point) = D. Для расстояний и Project нужна нормаль длины 1. D — число в правой части, а не готовый вектор позиции. Все точки и нормаль в одной операции должны относиться к одному пространству.
| Плоскость | Точка | DistanceTo | Project |
|---|---|---|---|
| Normal = Up, D = 2, то есть Y = 2 | (1, 5, 3) | 3 | (1, 2, 3) |
| Та же плоскость | (0, 0, 0) | −2 | (0, 2, 0) |
Положительный знак — сторона, куда направлена Normal. При одновременной смене знака Normal и D геометрическая плоскость сохраняется, а знак расстояния меняется. Конструктор Plane(normal, d) не нормализует переданные числа автоматически. Для Normal = 2 × Up, D = 4 DistanceTo точки с Y = 5 даст 6; после Normalized получится правильное расстояние 3. Нормализуются и нормаль, и D. Нулевой вектор не задаёт корректную нормаль плоскости.
Увидеть проекцию в 3D
Создайте отдельный C#-уровень 1.0.0 с корнем Entity3D, добавьте TransformRig3D.cs и прикрепите к корню. Поставьте окно/viewport 640×420, соберите и нажмите F6. Синий куб задан локальной точкой (1, 1, 0) под повёрнутым и масштабированным родителем. Белая метка — проекция его GlobalPosition на мировую плоскость Y = 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;
}
}Сначала получайте мировую позицию куба, затем применяйте мировую Plane. Проекция локальной Marker.Position на эту мировую плоскость смешала бы пространства. TryTransformNormal в файле относится к переносу нормали при масштабе; он не нужен для уже заданной мировой Up.
Луч и отрезок
IntersectsRay(from, dir) возвращает Vector3?; проверьте наличие значения до использования. Для Y = 2 луч из (0, 5, 0) вниз попадает в (0, 2, 0), вверх — нет. Параллельный луч возвращает null. Это бесконечный луч только вперёд, не прямая в обе стороны.
IntersectsSegment(begin, end) ограничивает пересечение концами отрезка. От Y = 5 до Y = 0 пересечение есть, от 5 до 4 — нет. Совпавшие или лежащие в плоскости бесконечно многие точки не превращаются в единственное попадание. Для почти параллельных направлений учитывайте численный допуск реализации.
Plane из трёх точек зависит от их порядка. В проверенном C#-конструкторе точки Zero, Right, Up дают нормаль Forward (−Z). Не предполагайте знак по привычке из другого формата. Коллинеарные точки не задают нормальную плоскость.
Эти операции не проверяют коллайдеры, слои, видимость или занятость места в мире. Для выбора поверхности игры используйте физический запрос; Plane подходит для геометрического расчёта известной плоскости.