You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于C++与SFML的简易3D引擎:带相机旋转的透视投影咨询

嘿,很高兴看到你已经用C+++SFML搭起了简易3D引擎的基础,固定相机的透视投影还跑起来效果不错!要给相机加上旋转功能,核心思路其实是先把世界空间的3D点转换到相机的局部坐标系下,再复用你现有的投影公式就行——本质上是让“相机转向”的效果等价于“所有3D点围着相机反向旋转”,下面给你拆解具体步骤:

核心逻辑:世界点 → 相机空间点 → 屏幕点

你当前的投影公式是基于相机固定朝向(应该是沿着Z轴方向)的,要支持旋转,就得先把世界坐标系里的点,变换到以相机为原点、相机朝向为-Z轴的局部坐标系中,再做投影。


步骤1:先把点平移到相机原点

把世界空间中的点P,减去相机的世界位置E,得到以相机为原点的平移后点:

// 假设用自定义结构体或sf::Vector3f存储3D坐标
struct Vec3 { float x, y, z; };

Vec3 translateToCameraOrigin(Vec3 worldPoint, Vec3 cameraPos) {
    return {
        worldPoint.x - cameraPos.x,
        worldPoint.y - cameraPos.y,
        worldPoint.z - cameraPos.z
    };
}

步骤2:给平移后的点做旋转变换

相机的旋转一般用三个欧拉角控制:

  • 偏航角(Yaw):绕Y轴左右转(对应左右看)
  • 俯仰角(Pitch):绕X轴上下转(对应抬头低头)
  • 滚转角(Roll):绕Z轴翻转(简易引擎大多用不上,可先忽略)

我们需要对平移后的点,按照相机旋转的逆方向做旋转,这样就等价于相机“转向”了这些点。下面是两个核心旋转的实现(用弧度计算,C++的cos/sin默认接收弧度):

// 绕Y轴旋转(Yaw)
Vec3 rotateY(Vec3 point, float yawRadians) {
    float cosY = cos(yawRadians);
    float sinY = sin(yawRadians);
    return {
        point.x * cosY + point.z * sinY,
        point.y,
        -point.x * sinY + point.z * cosY
    };
}

// 绕X轴旋转(Pitch)
Vec3 rotateX(Vec3 point, float pitchRadians) {
    float cosX = cos(pitchRadians);
    float sinX = sin(pitchRadians);
    return {
        point.x,
        point.y * cosX - point.z * sinX,
        point.y * sinX + point.z * cosX
    };
}

// 组合平移+旋转,得到相机空间下的点
Vec3 worldToCameraSpace(Vec3 worldPoint, Vec3 cameraPos, float yaw, float pitch) {
    Vec3 translated = translateToCameraOrigin(worldPoint, cameraPos);
    Vec3 rotated = rotateY(translated, yaw);
    rotated = rotateX(rotated, pitch);
    return rotated;
}

步骤3:用修改后的投影公式转屏幕点

现在相机空间的原点就是相机位置,你原来的公式可以简化——把Ez换成焦距f(对应相机到投影平面的距离,和你原来的Ez作用一致),最后加上屏幕中心坐标(因为SFML屏幕坐标系左上角是原点,投影点要偏移到屏幕中心):

// 示例参数:屏幕尺寸800x600,焦距200
const sf::Vector2i SCREEN_CENTER = { 800/2, 600/2 };
const float FOCAL_LENGTH = 200.0f;

sf::Vector2f projectToScreen(Vec3 cameraSpacePoint) {
    // 裁剪:如果点在相机后方,直接跳过渲染(避免异常值)
    if (cameraSpacePoint.z <= -FOCAL_LENGTH) {
        return { -1, -1 }; // 标记为无效点
    }

    // 透视投影计算
    float screenX = (FOCAL_LENGTH * cameraSpacePoint.x) / (FOCAL_LENGTH + cameraSpacePoint.z) + SCREEN_CENTER.x;
    float screenY = (FOCAL_LENGTH * cameraSpacePoint.y) / (FOCAL_LENGTH + cameraSpacePoint.z) + SCREEN_CENTER.y;

    return { screenX, screenY };
}

完整流程示例

把上面的步骤串起来,就是一个带旋转相机的渲染逻辑:

// 相机状态
Vec3 cameraPos = { 0.0f, 0.0f, 5.0f };
float cameraYaw = 0.5f; // 向右转约28.6度(弧度=角度*M_PI/180)
float cameraPitch = 0.2f; // 抬头约11.5度

// 世界空间中的测试点
Vec3 testPoint = { 1.0f, 1.0f, 0.0f };

// 转换到相机空间
Vec3 camPoint = worldToCameraSpace(testPoint, cameraPos, cameraYaw, cameraPitch);

// 投影到屏幕
sf::Vector2f screenPoint = projectToScreen(camPoint);
if (screenPoint.x != -1) {
    // 用SFML画点
    sf::CircleShape pixel(2);
    pixel.setFillColor(sf::Color::White);
    pixel.setPosition(screenPoint);
    window.draw(pixel);
}

额外注意事项

  1. 角度转弧度:如果你的相机控制用的是角度(比如鼠标移动对应多少度),记得转换成弧度:radians = degrees * M_PI / 180.0f,需要包含<cmath>头文件。
  2. 万向锁问题:欧拉角会存在万向锁(比如抬头90度后左右转变成滚转),但如果是简易引擎的基础需求,完全够用;如果要做复杂相机运动,后续可以考虑用四元数替代。

内容的提问来源于stack exchange,提问作者user9043613

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 04:03:27