
本文详解如何在纯 JavaScript 的 2D Canvas 中实现“伪 3D”(2.5D)效果,核心是通过三角函数将世界坐标转换为相机视角下的相对坐标,确保精灵始终面向相机(billboarded),不依赖 ctx.rotate()。
本文详解如何在纯 javascript 的 2d canvas 中实现“伪 3d”(2.5d)效果,核心是通过三角函数将世界坐标转换为相机视角下的相对坐标,确保精灵始终面向相机(billboarded),不依赖 `ctx.rotate()`。
在 2.5D 渲染中,“2D 绘制 + 3D 感知”是关键——所有精灵仍以 2D 方式绘制(如 drawImage),但其屏幕位置需根据虚拟相机的朝向动态重算,使其视觉上“固定朝向镜头”,即 billboard 效果。这与直接调用 ctx.rotate() 有本质区别:后者会旋转图像本身(破坏朝向),而前者仅平移绘制坐标,保持精灵始终正对观察者。
核心原理:极坐标视角变换
假设世界中某精灵位于全局坐标 (worldX, worldY),相机位于 (camX, camY),且相机朝向角度为 camAngle(单位:弧度,0° 指向右,逆时针递增)。我们需计算该精灵在相机“前方平面”上的投影位置(即人眼所见的相对位置):
-
求相对偏移向量:
const dx = worldX - camX; const dy = worldY - camY;
-
转为极坐标(距离 + 相对角度):
const distance = Math.sqrt(dx * dx + dy * dy); const angleFromCam = Math.atan2(dy, dx); // 注意:atan2(y,x) 返回 [-π, π]
-
叠加相机朝向,得到最终屏幕方向角:
const screenAngle = angleFromCam - camAngle; // 关键:减去相机朝向,实现“相对视角”
-
反解为屏幕坐标(以画布中心为原点):
const screenX = canvas.width / 2 + distance * Math.cos(screenAngle); const screenY = canvas.height / 2 + distance * Math.sin(screenAngle);
✅ 为什么是 angleFromCam - camAngle?
因为相机自身旋转了 camAngle,世界坐标系需“反向旋转”才能对齐相机局部坐标系。这是坐标系变换的本质:将点从世界空间变换到相机空间。
完整示例代码
<canvas id="gameCanvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 场景数据
const sprites = [
{ x: 50, y: 40, img: createPlaceholder(32, 32, '#4a90e2') },
{ x: 20, y: 70, img: createPlaceholder(32, 32, '#e74c3c') }
];
const camera = { x: 0, y: 0, angle: degToRad(35) }; // 35° 角度需转为弧度
function degToRad(d) { return d * Math.PI / 180; }
function createPlaceholder(w, h, color) {
const temp = document.createElement('canvas');
temp.width = w; temp.height = h;
const tctx = temp.getContext('2d');
tctx.fillStyle = color; tctx.fillRect(0, 0, w, h);
return temp;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
sprites.forEach(sprite => {
// 步骤1:计算相对于相机的偏移
const dx = sprite.x - camera.x;
const dy = sprite.y - camera.y;
// 步骤2:极坐标转换
const dist = Math.sqrt(dx * dx + dy * dy);
const relAngle = Math.atan2(dy, dx);
// 步骤3:转换到相机局部坐标系
const screenAngle = relAngle - camera.angle;
// 步骤4:映射到屏幕(以画布中心为参考点)
const screenX = canvas.width / 2 + dist * Math.cos(screenAngle);
const screenY = canvas.height / 2 + dist * Math.sin(screenAngle);
// 绘制(始终正向,无旋转)
ctx.drawImage(sprite.img, screenX - 16, screenY - 16);
});
}
// 启动渲染循环
requestAnimationFrame(() => {
render();
});
</script>注意事项与进阶提示
- Z 排序(深度):若需正确遮挡,应按 dist(到相机距离)降序绘制精灵,近的后画。
- 缩放模拟远近:可引入 scale = 1 / (1 + dist * 0.01) 等简单公式,让远处精灵变小,增强纵深感。
- 性能优化:避免在每帧重复创建临时 canvas;预生成 sprite 图像并复用。
- 角度单位统一:JavaScript 三角函数均使用弧度,务必用 degToRad() 转换输入角度。
- 边界处理:dist === 0 时 atan2 无定义,建议添加 if (dist
掌握这一变换逻辑,你便拥有了构建等距、斜向或自由旋转 2.5D 场景的底层能力——它不依赖任何引擎,纯粹基于几何直觉与基础三角学,正是 Winterwood 类复古风格游戏的核心技术基石。











