像1993年那样制作图形

Hacker News Top 新闻

摘要

一位开发者详细介绍了如何构建《Catlantean 3D》——一款采用1993年时代图形技术(256色、320x240分辨率、手工制作资产、无人工智能)的第一人称射击游戏,计划在Steam上发布,重点讲解调色板渲染和资产创建。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/09 11:41

# Catlantean 3D——像1993年那样制作图形 来源:https://staniks.github.io/articles/catlantean-3d-blog-1/ Catlantean 3D 是我在过去一年多的时间里,利用业余时间慢慢构建的一个副产品,我打算明年在 Steam 上发布它。 您的浏览器不支持视频标签。我的目标是使用 90 年代初期常见的技巧,构建一个完整的、可发行的第一人称射击游戏,同时允许自己使用现代编译器和平台抽象层。 这实际上意味着,我愚蠢地给自己施加了以下限制: - 游戏必须完全从零开始制作,包括所有资产 - 所有渲染必须手工完成 - 所有混音必须手工完成 - 目标分辨率为 320x240 - 仅限 256 色 - 允许使用浮点数,但行为必须在各平台上一致——游戏逻辑决定采用定点数以保证确定性行为,渲染则使用浮点数,因为确定性在那里不那么重要 - 必须是一款完成度高、打磨精良、玩起来有趣的游戏(而非技术演示) - 允许使用平台抽象层,但我必须假装它功能非常有限(合理范围内):- 帧缓冲区,用于写入像素 - 键盘/鼠标输入 - 音频缓冲区,用于写入样本 - 文件系统 I/O - 不得使用 AI 生成的糟粕 如果你觉得这听起来不合理,那是因为它确实不合理。 但我无论如何都要做。今天我要讲讲开发博客中通常被忽视的一点:资产创建。 **注:** 这里展示的所有内容都处于开发阶段,可能会有很大变化。 ## 目录 1. 调色板渲染 (https://staniks.github.io/articles/catlantean-3d-blog-1/#palette-rendering)1. VGA 图形 (https://staniks.github.io/articles/catlantean-3d-blog-1/#vga-graphics) 2. 调色板 (https://staniks.github.io/articles/catlantean-3d-blog-1/#the-palette) 3. 颜色映射表 (https://staniks.github.io/articles/catlantean-3d-blog-1/#the-colormap) 2. 创建资产 (https://staniks.github.io/articles/catlantean-3d-blog-1/#creating-assets)1. 预渲染精灵 (https://staniks.github.io/articles/catlantean-3d-blog-1/#pre-rendered-sprites) 2. 手绘精灵与纹理 (https://staniks.github.io/articles/catlantean-3d-blog-1/#hand-drawn-sprites) 3. 程序化生成的精灵与纹理 (https://staniks.github.io/articles/catlantean-3d-blog-1/#procedurally-generated-sprites) 3. 地图 (https://staniks.github.io/articles/catlantean-3d-blog-1/#maps) 4. 结论 (https://staniks.github.io/articles/catlantean-3d-blog-1/#conclusion) ## 更新日志 - **2026-06-09** - 发布。 ## 调色板渲染 ## VGA 图形 VGA 硬件上的 Mode 13h 是著名的 320x200 256 色图形模式,定义了一代 PC 游戏。从程序员的角度来看,它简单得令人惊叹:你有一个线性帧缓冲区,每个像素由一个字节表示,该字节索引到一个包含 256 种颜色的调色板中。 如果你想画一个像素,就向特定地址写入一个字节,仅此而已,没有着色器、VRAM 之类的概念。 一个像素一个字节,这个字节是调色板中的索引,调色板包含实际显示到屏幕上的 RGB 值。这带来了一些有趣的限制;在为现代游戏制作资产时,你可以给图像使用数百万种颜色,但当屏幕上每个像素只能从 256 种颜色中选择时,资产创建就变成了一个截然不同的问题,因为每种颜色选择都必须谨慎且有意为之。 像《毁灭战士》和《毁灭公爵》这样的游戏就是这方面做得好的例子。这些图形之所以具有某种清晰感和锐利感,正是*因为*这些技术限制,而不是无视它们。限制迫使你做出深思熟虑的选择,而深思熟虑的选择往往看起来不错。 Catlantean 3D 试图重现这种感觉,但有一个例外——我实际上追求的是更接近 VGA Mode-X,即 320x240。原因是,如果在 4:3 显示器上显示 320x200,最终像素不是方的!虽然这会更真实,但由于个人偏好而非客观原因,我选择不处理这个问题。 那么,如何创建符合这些限制的图形呢? ## 调色板 一切从 768 个字节开始,经过多次迭代试错精心挑选。 选择这些特定颜色的主要原因如下: - 一种颜色保留给透明(鲜艳的粉色) - 一种颜色保留给纯白 - 一种颜色保留给纯黑 - 显然我需要很多血,因此需要红色 - 绿色和蓝色的阴影,因为我会有红、绿、蓝三色钥匙和对应颜色的门 - 游戏背景设定在 Catlantis,这是一个模仿古埃及的恶搞国度(因为猫崇拜),所以显然需要大量沙漠色调(黄色和棕色) - 大量灰色,因为场景涉及许多技术设施(Catlantis 被机械狗人占领) - 一些米色色调,打破灰色的单调,并在变暗时作为更温暖的替代色(稍后详述) - 其余部分在创建纹理时根据需要填充——高度主观,难以解释,只能说“看起来对了” 调色板并非一蹴而就;在资产创建、测试和整体重复迭代过程中,经历了大量来回调整。 下面是实际游戏中精灵和纹理的一些示例: ## 颜色映射表 Catlantean 3D 是一款传统的光线投射游戏。地图由大小相同的瓦片组成;有些是墙壁,另一些只是带有地板和天花板的空洞。为了渲染地图,渲染器对每一列屏幕使用 DDA 算法,遍历瓦片地图并确定它与地图几何体的交点,然后根据交点,在屏幕上渲染一列墙壁,使用适当的纹理,并从适当的坐标采样。地板和天花板随后作为水平扫描线渲染,填充屏幕的其余部分。 光线投射已经被其他博客和网站讲烂了,所以我不会全部覆盖,但我想谈谈我认为最容易被忽视的一个方面:光照。 如果我们只用调色板渲染游戏世界,没有任何特效,最终会得到相当平淡无奇的效果: 但我们需要的是下面这样的效果。注意,随着几何体距离玩家越远,光线会减弱,而且地图瓦片的其中一面比另一面只暗了一点点。这给人带来了深度感。 使用现代硬件加速渲染器,这可以简单地在着色器中完成——根据顶点距离,将其颜色向量乘以一个浮点因子,从而得到变暗后的颜色向量。 但是,使用调色板渲染器如何实现类似效果呢?它没有颜色的概念,只有调色板的索引。所以,如果我们想找到某种颜色的更暗阴影,需要遍历整个调色板,找到符合“更暗”标准的颜色。这太耗性能了,因为我们不能为屏幕上渲染的*每个*像素都遍历整个调色板,那样太慢了。 我们可以做的是进行一些预处理,以便在运行时根据距离快速进行颜色查找。 如果我们像这样将调色板排列成单行…… 然后选择阴影级别数量(我选了 32 个),意味着每种颜色需要 31 个更暗的变体,全部*从调色板中*获取。我们知道每种颜色的 RGB 值,因此可以根据这些值和阴影索引确定该阴影的最接近目标颜色: `` // 第一个阴影索引 (0) 是原始颜色。 float darkening_factor = (32 - shade_index) / 32.0f; target_darker_color.r = current_color.r * darkening_factor; target_darker_color.g = current_color.g * darkening_factor; target_darker_color.b = current_color.b * darkening_factor; `` 但这个颜色可能不在调色板中。所以我们需要遍历调色板,找到最接近它的颜色。 在开发过程中,“接近”的定义实际上发生了变化——起初,我使用欧几里得距离作为度量,但问题在于,仅仅由于数学原因,几乎所有颜色都倾向于偏向灰色。一些老游戏确实使用欧几里得距离,但对我来说效果不太好。我无法确切解释为什么,但许多较暗的阴影显得有点冷寂、缺乏生气。所以,我转而将颜色转换到 Oklab 色彩空间,并利用其感知距离公式,该公式更接近人类对颜色差异的感知。我还对颜色越暗的部分应用轻微的暖色调偏移(像素艺术中常见的一种技巧,称为“色相偏移”)。这通常不是必需的,但它确实能让游戏看起来好一点。 在这种情况下,我如何定义“更好”?我不知道,只是看起来对了。令人沮丧,不是吗?主观的东西很难理性化。 回到我们的算法……本质上,对于每种颜色,我们创建一个代表该颜色阴影的列。最终得到的是一个二维的调色板索引矩阵,称为颜色映射表。注意,颜色映射表的渐变并不完美,因为我们仍然受限于调色板中的颜色: 所以现在,根据距离确定颜色`N`的更暗阴影变得非常简单。 给定基于距离的颜色映射表行索引(即阴影级别): `` colormap_row = 32 * fragment_distance / view_distance `` 我们从属于该阴影的行中选择第 N 个条目——那就是变暗后的颜色`N`的调色板索引。 瞧,O(1) 复杂度。 此外,我们不是为每个像素计算颜色映射表行索引,而是通过以下方式进一步降低成本: - 渲染墙壁时,每列屏幕只计算一次,因为墙壁完全垂直,该列中每个像素到相机的距离相同 - 渲染地板时,每行屏幕只计算一次,因为地板完全水平,该行中每个像素到相机的距离相同 - 每个精灵只计算一次,因为精灵是完全平坦的公告板,每个像素到相机的距离相同 因此,墙壁的光线投射计算颜色映射表行索引 320 次,地板最多 240 次,每个可见精灵一次(光线投射提供了免费的遮挡剔除)。这很廉价,收益却很大。 《毁灭战士》和许多其他游戏使用了类似的方法。 ## 创建资产 Catlantean 3D 中的纹理和精灵分为三类: 1. 预渲染精灵——在 Blender 中创建 3D 模型并渲染为纹理 2. 手绘精灵和纹理 3. 程序化生成的纹理——通过结合手绘美术,由特殊 Python 脚本生成 ## 预渲染精灵 我有一份全职工作,生活还算活跃,所以用于开发游戏的时间有限。因此,我希望在制作涉及动画的复杂精灵时,尽量减少重复迭代的时间。我很少能一次做对,所以重复迭代是预料之中的,而当需要改动动画的许多帧时,重复迭代很困难。 更高效的方法是在 Blender 中将精灵创建为 3D 模型,在那里进行绑定和动画制作,然后利用 Blender 的 Python API,通过特殊脚本将其渲染为一系列纹理。重复迭代仅涉及修改模型,渲染脚本会完成其余工作,这节省了大量时间。 主要障碍是渲染出的精灵非常模糊、褪色。 有人可能认为,显而易见的答案是先渲染高分辨率精灵,然后通过滤镜进行降采样,但我的成功经验不一;细节往往会被滤镜抑制,边缘清晰度也会丢失。我发现最有效且可重复利用的方法是借助 Blender 的合成功能来获得合适的对比度和清晰度: 图像准备好后,通过一个特殊的 Python 脚本进行调色板量化,生成引擎使用的每像素 1 字节图像。对于源图像中的每个像素,脚本会找到调色板中最接近的颜色(感知上最接近——Oklab 色彩空间),并使用该颜色对应的索引。索引数组及尺寸随后被打包成游戏使用的非常简单的 TEX 格式。 类似的流程也用于敌人精灵。注:其中一些节点要么多余,要么完全无用,只是因为我曾在某个时候使用过它们,后来又改变了主意。我喜欢保留它们,以防再次需要。 敌人精灵以特殊方式渲染。精灵可以有多个动画,每个动画必须为精灵可以面向的 8 个方向分别提供帧。因此,对于每个动画(行走、开火、死亡等),使用 Blender API 的 Python 脚本会旋转精灵,渲染动画的所有帧,再次旋转精灵,依此类推。精灵保存时带有特殊命名约定,表示精灵名称、动作名称、方向和帧索引: 这种方法的好处是,我不需要将渲染后的精灵保存在仓库中——它们实际上被 .gitignore 忽略了。每当我换地方使用另一台电脑时,只需运行编译脚本,它就会渲染每个模型并生成精灵。速度相当快,在 RTX 3070 上约为 15 个模型 10 秒左右。 ## 手绘精灵与纹理 在开发早期,我用我的猫 Vilko 的纹理制作了一个模糊的猫形头,用作状态栏面部。毕竟,我为什么要手工绘制这样的东西呢,既然 Blender 可以如此生动地再现生命? 显而易见的答案是,它看起来很懒惰、不用心,事实也确实如此。它在传达情感方面做得不好,也没有灵魂。当我收集关于氛围的反馈时,这通常是最先被人指出的事情。 有些事情就是需要手工绘制。我不是艺术家,但我很有信心手绘版本加上动画后看起来好得多,而如果我在 Blender 中制作这个动画,永远无法重现同样的效果。由于精灵的尺寸,每个像素都需要深思熟虑,所以没有理由把这个工作留给 Blender 渲染器。 我将同样的逻辑应用于大多数拾取物品,这些以前是预渲染的,但在 Blender 合成器无法可靠产生良好效果的尺度下。经过人工处理后,它们的清晰度和可读性大大提高了。 你可能会想,为什么不干脆提高精灵分辨率呢?游戏光栅化器会自行处理缩放,对吧? 嗯,这能行,但结果会很糟糕,因为像素尺度不再一致。在屏幕的任何一行或一列,你会下意识地期望像素在你靠近或远离它们时保持相同的尺寸。如果像素尺度在不同的精灵之间变化,这一期望就不成立,看起来就会很别扭。这可能是许多资产倒卖者或低努力独立游戏看起来糟糕的最大原因之一;他们拼凑不同尺度的资产,这些资产根本无法协同工作。 因此,Catlantean 3D 世界中的一个单位是 64 像素,每个精灵都相对于这个比例制作。所以如果我们想要一个高度为一个世界单位四分之一的精灵,它必须是 64/4=16 像素高。 ### HUD HUD 及其元素几乎完全是手绘的。这包括: - 屏幕底部的状态栏 - 各种过渡面板和屏幕 - 字体 例如,这个正在制作中的关卡结束评分屏幕: 您的浏览器不支持视频标签。HUD 是手

相似文章

Making a game in Visual Studio from 1997

Lobsters Hottest

一名资深开发者使用1997年的Visual Studio 6和纯C语言在Windows 7上搭建了双摇杆射击游戏框架,展示了固定时间步长、对象管理和OpenGL兼容模式等复古开发实践。

像1997年那样编译Quake!

Fabien Sanglard

一份详细的指南,介绍如何重现使用Windows NT 4和Visual C++ 6等老式工具编译Quake的win32二进制文件的过程(就像1997年所做的那样)。

现代渲染剔除技术

Hacker News Top

本文由 Saints Row: The Third Remastered 的一位开发者撰写,详细讲解了包含距离剔除、背面剔除和视锥体剔除在内的现代渲染剔除技术,并为致力于实时图形优化的游戏开发人员提供了宝贵的实践经验。