基于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); }
额外注意事项
- 角度转弧度:如果你的相机控制用的是角度(比如鼠标移动对应多少度),记得转换成弧度:
radians = degrees * M_PI / 180.0f,需要包含<cmath>头文件。 - 万向锁问题:欧拉角会存在万向锁(比如抬头90度后左右转变成滚转),但如果是简易引擎的基础需求,完全够用;如果要做复杂相机运动,后续可以考虑用四元数替代。
内容的提问来源于stack exchange,提问作者user9043613
相关产品推荐
相关产品推荐

