2D 等轴测视角:完整理论

05/2584 浏览开发心得
2D 等轴测视角:完整理论
一、视角的本质
什么是等轴测
人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。
俯视(正上方看下去):         等轴测(斜上方看下去):
  ┌──┬──┬──┐                      ◇
  ├──┼──┼──┤                    ◇   ◇
  ├──┼──┼──┤                  ◇   ◇   ◇
  └──┴──┴──┘                    ◇   ◇
                                  ◇
正方形网格经过旋转 + 压缩,变成了菱形网格。
为什么是 2:1
业界最通用的等轴测比例是菱形宽:高 = 2:1。
        ╱╲         宽 = 2 单位
       ╱  ╲        高 = 1 单位
      ╱    ╲
     ╱      ╲
    ╲      ╱
     ╲    ╱
      ╲  ╱
       ╲╱
为什么选 2:1:
比例 视觉效果 问题
1:1 正菱形,像扑克牌方块♦ 太陡,像从很低的角度看
2:1 最经典,视野开阔 无
3:1 非常扁,接近俯视 建筑太矮,缺乏立体感
2:1 对应的理论观察角度约为 30°(从水平线往上抬 30° 的仰角去看),但在像素游戏中不需要精确角度,只需要记住"宽是高的两倍"这一条规则。
投影公式
这是整个系统的数学基石。假设每个格子宽 W、高 H(W = 2H):
网格坐标 (col, row) → 屏幕坐标 (sx, sy)
sx = (col - row) × W/2
sy = (col + row) × H/2
逆变换(屏幕坐标反推网格坐标):
col = (sx / (W/2) + sy / (H/2)) × 0.5
row = (sy / (H/2) - sx / (W/2)) × 0.5
直觉理解:
col 增大 → 屏幕往右下走
row 增大 → 屏幕往左下走
col 和 row 同时增大 → 纯粹往屏幕下方走
col 增大 row 减小 → 纯粹往屏幕右方走
二、地图的制作原理
层级结构
等轴测地图不是一张大图片,而是由多层数据叠加而成:
┌─────────────────────────────┐
│  第四层:UI / HUD           │  ← 固定在屏幕上,不随地图滚动
├─────────────────────────────┤
│  第三层:角色 / NPC          │  ← 按 Y 坐标排序(遮挡关系)
├─────────────────────────────┤
│  第二层:建筑 / 障碍物       │  ← 有高度的物体
├─────────────────────────────┤
│  第一层:地面瓦片            │  ← 最底层,铺满整个地图
└─────────────────────────────┘
地面层:瓦片铺设
地面是一个二维数组,每个格子存一个类型编号:
tileType[row][col] = GRASS / ROAD / WATER / PLAZA / ...
渲染时遍历可见范围内的格子,根据类型选择对应的菱形贴图:
对于每个可见格子 (col, row):
    1. 用投影公式算出屏幕坐标 (sx, sy)
    2. 查表得到瓦片类型 → 选择贴图
    3. 在 (sx, sy) 处绘制菱形贴图
关键优化:只绘制屏幕可见区域。一张 281×242 的地图有 6.8 万格子,全部遍历太慢。通过相机位置和缩放级别,反算出可见的 col/row 范围,只遍历这个子集。
道路系统:参数化定义
道路不是逐格手工填充,而是用参数化线段定义:
一条道路 = {
    方向:   水平 或 垂直,
    中心线: 第几行/列,
    起止:   从第几格到第几格,
    宽度:   占几格,
}
示例:一条宽 5 格的水平主干道
       中心线 coord = 50
  ──────────────────────────
  │  row 48  │░░░░░░░░░░│    ← coord - 2
  │  row 49  │░░░░░░░░░░│
  │  row 50  │░░░░░░░░░░│    ← 中心
  │  row 51  │░░░░░░░░░░│
  │  row 52  │░░░░░░░░░░│    ← coord + 2
  ──────────────────────────
             from → to
判断某格是否是道路:
for 每条道路 do
    half = floor(宽度 / 2)
    if 水平道路 then
        if row 在 [coord-half, coord+half] 且 col 在 [from, to] then
            → 是道路
    else -- 垂直道路
        if col 在 [coord-half, coord+half] 且 row 在 [from, to] then
            → 是道路
这种方式用十几条数据就能定义整个城市的路网,比逐格填充高效几个数量级。
区域系统:矩形标记
建筑、广场、水域等用矩形区域定义:
一个区域 = {
    左上角: (col1, row1),
    右下角: (col2, row2),
    类型:   建筑 / 广场 / 水域,
    名称:   "xxx",
}
查询某格属于什么区域:
function getTileInfo(col, row)
    -- 优先判断道路(道路覆盖在区域之上)
    for 每条道路 do
        if 在道路范围内 then return "道路", 可走
    end
    -- 再判断区域
    for 每个区域 do
        if col 在 [x1, x2] 且 row 在 [y1, y2] then
            return 区域类型, 是否可走
        end
    end
    -- 默认是草地
    return "草地", 可走
end
通行性
每种瓦片类型有固定的通行规则:
类型 可通行 说明
草地 是 默认地面
道路 是 主要行走路线
广场 是 开阔区域
建筑 否 角色绕行
水域 否 不可踏入
城门 是 出入口
三、建筑的制作原理
等轴测建筑的核心思想:在菱形底面上向上"挤出"墙壁,再盖上屋顶。虽然整个画面是纯平面绘制,但通过几何技巧模拟出立体感。
步骤一:确定地面菱形
建筑占据一个矩形区域 (col1,row1) 到 (col2,row2),投影到屏幕后是一个菱形:
                 顶点 (col1,row1)
                    ◇
                  ╱    ╲
       左点    ╱        ╲   右点
   (col1,row2+1)        (col2+1,row1)
                ╲        ╱
                  ╲    ╱
                    ◇
              底点 (col2+1,row2+1)
4 个角的屏幕坐标用投影公式算出。
步骤二:向上挤出墙壁
将地面菱形的 4 个顶点各向屏幕 Y 方向上移 wallHeight 像素,得到顶面 4 点:
        顶面 (上移 wallH)
            ◇ ─ ─ ─ ─ ─ ◇
          ╱ │           ╱ │
        ╱   │         ╱   │
      ◇     │       ◇     │    ← 只画可见的两个面
        ╲   │         ╲   │       (左墙面 + 右墙面)
          ╲ │           ╲ │
            ◇ ─ ─ ─ ─ ─ ◇
        地面菱形
等轴测视角下,只能看到建筑的两个侧面:
左墙面(西南面):顶点连线为 左点→底点→底点上方→左点上方
右墙面(东南面):顶点连线为 底点→右点→右点上方→底点上方
北面和西面被自身遮挡,不需要画。
步骤三:光影区分
两个可见面用不同明度区分,制造光照感:
光源假设在右上方:
  左墙面(背光):基色 - 35 明度    较暗
  右墙面(受光):基色 + 8 明度     较亮
假设墙壁基色 RGB = (190, 170, 140)
左墙: (155, 135, 105)   ← 减 35,阴影面
右墙: (198, 178, 148)   ← 加 8,受光面
仅这一步就能让平面绘制产生很强的立体感。
步骤四:添加屋顶
两种常见屋顶形态:
尖顶(四棱锥):
              尖顶点
                △
              ╱ │ ╲
            ╱   │   ╲
          ╱     │     ╲
     左上 ╱─────│─────╲ 右上
          ╲     │     ╱
            ╲   │   ╱
              ╲ │ ╱
               底上
  尖顶点 = 顶面中心再上移 peakHeight
  4 个三角面,分别用不同深浅的屋顶色
平顶:
     顶面直接画一个填充菱形
         ◇
       ╱    ╲
     ◇      ◇
       ╲    ╱
         ◇
步骤五:装饰细节
在墙面上叠加细节增加真实感:
砖缝线:
  沿墙面水平方向画几条等距细线
  颜色比墙面暗 20-30,半透明
  模拟石砖或木板纹理
门:
  右墙面底部中央画一个深色矩形
  宽度约占墙面 16%,高度约占墙高 45%
窗户(可选):
  墙面上方画小矩形,浅色描边
步骤六:阴影
在地面菱形偏移 (3, 3) 像素处画一个半透明黑色菱形,模拟地面投影:
  真实菱形位置
      ◇
    ╱    ╲         ◇ ← 阴影 (偏移3px, 黑色半透明)
  ◇      ◇      ╱    ╲
    ╲    ╱     ◇      ◇
      ◇          ╲    ╱
                   ◇
先画阴影,再画建筑本体,阴影就自然出现在建筑边缘。
建筑配色体系
不同功能的建筑用不同色系区分:
建筑类型 墙面色调 屋顶色调 屋顶形态
城堡/官方 灰蓝石 深蓝 平顶
商店 暖砂岩 红瓦 尖顶
民居 木色 红棕 尖顶
宗教 白石 铜绿 尖顶
竞技/军事 砂石 灰褐 平顶
公共服务 浅石 棕瓦 尖顶
这样玩家不看文字标注,光凭颜色就能大致分辨建筑功能。
四、绘制顺序(遮挡关系)
等轴测最关键的问题:谁画在谁前面?
规则很简单:先画远的,后画近的。"远近"的判断标准是 col + row 的值(即屏幕 Y 方向):
绘制顺序(从远到近):
  row=0, col=0  ← 最先画(最远,屏幕最上方)
  row=0, col=1
  row=1, col=0
  row=1, col=1
  ...
  row=N, col=M  ← 最后画(最近,屏幕最下方)
画面叠加效果:
      远处建筑 (先画)
          ┊
    近处角色 (后画,覆盖在建筑下半部分之上)
          ┊
    最近的树 (最后画,覆盖角色)
对于建筑和角色混合的场景,统一按 col + row(或直接用屏幕 Y 坐标)排序后依次绘制,自然就形成了正确的前后遮挡。
五、完整渲染流水线
每一帧:
1. 清屏(填充背景色)
         │
2. 计算可见网格范围(根据相机位置和缩放)
         │
3. 绘制地面层
   │  遍历可见格子,逐行逐列
   │  根据类型铺菱形贴图(草地/道路/广场/水面)
         │
4. 收集所有需要绘制的立体物体
   │  建筑、角色、NPC、树木……
   │  按 (col + row) 或屏幕 Y 排序
         │
5. 按排序顺序依次绘制
   │  先画阴影 → 墙壁(暗面+亮面)→ 屋顶 → 装饰
         │
6. 绘制 UI 层(固定在屏幕上,不随地图移动)
总结
概念 核心原理
视角 正交网格旋转 45° + Y 轴压缩 50%,菱形宽高比 2:1
投影 sx = (col-row)×W/2,sy = (col+row)×H/2,及其逆变换
地图 多层叠加:地面瓦片 + 道路参数化线段 + 区域矩形标记
建筑 地面菱形 → 向上挤出墙壁 → 光影分
TapTap
TapTap
面 → 加屋顶 → 加装饰
遮挡 按 col + row 从小到大绘制,后画覆盖先画
交互 屏幕坐标逆投影回网格坐标,判断通行性后移动角色
这套体系从 90 年代的《暗黑破坏神》《仙境传说》到现在的独立游戏一直在用,原理完全相同,区别只在于美术精度和优化手段。
4
6