MIT 许可证 NuGet

首页 / 文档 / 放置与相机

放置、锚点与相机

一条约定同时覆盖 2D 与 3D、单个对象与实例: 位置是锚点的世界位置,而 宽、高与深是目标尺寸 — 不是缩放系数。 锚点是对象自身包围盒的几何中心。

你会用到的成员

成员含义
PosX / PosY / PosZ锚点的世界位置。左手坐标系;在默认相机下 +X 是屏幕右侧。
Width / Height / Depth期望的世界尺寸。引擎从资产自身的边界推导缩放。它们是 float? — null 意味着“尚未设置”。
Rotation在 Model 上是绕 Y 轴的弧度。Mesh3D 与实例类型接受四元数。
Alpha不透明度,也是不移除某物而隐藏它的常用方式。
LocalSize只读。资产缩放前自身的尺寸,如果你需要按其单位推理的话。
ComputedScale只读。引擎推导出的逐轴缩放:目标尺寸除以局部尺寸。
AnchorLocal只读。模型空间中的锚点 — 原始包围盒的中心。
AnchorWorldOffset只读。当你需要放置模型的原点而不是中心时使用它;见下文。

回报是你可以为导入的资产设定尺寸,而不必知道美术是怎么导出的。 Width = 1f 就是一米宽,无论 GLB 声称什么,而且 两个来自两位美术的模型并排放置时,真的是一样高。

世界矩阵,写出来

每个 3D 控件都以同样的方式构建它的矩阵,而顺序正是全部要点:

Matrix4x4.CreateTranslation(-AnchorLocal)
    * Matrix4x4.CreateScale(ComputedScale)
    * GetRotationMatrix()
    * Matrix4x4.CreateTranslation(PosX, PosY, PosZ)

在行向量约定下,那就是 ((p - A) * S) * R + Pos。锚点 A 精确落在 Pos 上,旋转枢轴就是锚点,因此旋转 一个对象既不改变它的位置也不改变它的尺寸。正是这个性质让参考应用中的 对象编辑器无需任何补偿数学就能工作。

为什么锚点要在缩放之前减去

如果缩放在前,求值就会退化成 (p * S - A) * R + Pos:锚点将不再落在 Pos 上,世界中心会漂移 A * (1 - S),于是渲染、拾取与高亮边界 会彼此悄悄地不一致。锚点曾经也是一个角 (Min.X, Max.Y, Min.Z),直到它被改成 包围盒中心,因为角锚定给出有偏的旋转枢轴,让 放置难以思考。

放置原点而不是中心

有时你想把模型自身的原点钉在一个世界点上 — 例如, 一个导出时站在地面平面上的角色。恒等式 p*S*R + t == ((p-A)*S)*R + Pos 直接给你转换:

// Pin the model's local origin to a world-space target.
var target = new Vector3(4f, 0f, 12f);
var offset = robot.AnchorWorldOffset;   // recompute each frame

robot.PosX = target.X + offset.X;
robot.PosY = target.Y + offset.Y;
robot.PosZ = target.Z + offset.Z;

每一帧重新计算偏移量而不是缓存它:它已经把当前缩放和 旋转折叠进去了,所以对象变化时恒等式依然成立。先设置尺寸和旋转, 再设置位置。

三个值得提前知道、以免被它们惊讶的行为

  • 未设置的维度会收敛为资产自身的尺寸。 当边界在加载后确定时, 任何为 null 或 0 的 Width、 Height 或 Depth 都会从 LocalSize 填入,所以一个没有设置尺寸的 模型会以它导出的比例出现,而不是塌缩成空。
  • 扁平轴不会被缩放。 如果某个局部维度实际上是零 — 低于 1e-6,或比最大轴小四个数量级以上 — 该轴保持缩放 1。没有这个守卫,导出平面中的厚度噪声 会产生约 1e5 的缩放,把网格撕成偏移的碎片。这是一个 真实的资产,不是假设的。
  • 动画模型有两个包围盒。 GetWorldBounds() 返回用于 剔除和阴影的保守盒,为蒙皮运动做了膨胀。 GetWorldBoundsRaw() 与实际渲染的身体匹配。拾取和 选择框使用原始的那个;把保守盒用作选择框会在 模型上下留下可见的空白。

相机

BaseApp.Camera 是一个单一的 Camera3D,被每个后端和每个通道共享,它是 视图、投影与视锥的唯一事实来源。 CameraPos 和 CameraTarget 转发到 Camera.Position 和 Camera.Target。

CameraTarget = new Vector3(0f, 3.5f, -2.5f);
CameraPos = CameraTarget + new Vector3(0f, 0.5f, -3.5f);

Camera.FovY = MathF.PI / 4f;   // radians
Camera.Near = 0.1f;
Camera.Far = 1300f;
成员说明
Position、Target、Up左手系 look-at。写入相同的值不会弄脏矩阵。
FovY、Near、Far投影,运行时可变。Far 还钳制太阳级联延伸的距离。
Aspect只读;引擎每帧从表面提供它。
View、Projection、ViewProjection只在有东西变化时重建,这就是为什么每帧触碰相机是免费的。
Frustum剔除测试的对象。可供你做自己的粗略可见性检查。
ProjectionJittered、PrevViewProjection、JitterPixels归时间抗锯齿所有。如果你在调试 TAA 可以读它们;不要写它们。

当 CameraPos.Z 为负时,相机朝原点看, 世界 +X 是屏幕右侧 — 每个示例都用这个布置,2D 叠加坐标系也与它一致。