Making Graphics Like it's 1993

Making Graphics Like it’s 1993

像 1993 年那样制作图形

Catlantean 3D is a side-project I’ve been slowly building in my spare time for over a year, and I intend to release it on Steam next year. Catlantean 3D 是我利用业余时间缓慢构建的一项副业,已经持续了一年多,我打算明年在 Steam 上发布它。

My goal was to build a complete, shippable first-person shooter using techniques that were common in the early 90s, while allowing myself the luxury of using a modern compiler and a platform abstraction layer. What this actually means is, the constraints I have foolishly imposed upon myself are as follows: 我的目标是使用 90 年代初流行的技术构建一款完整、可发行的第一人称射击游戏,同时允许自己享受使用现代编译器和平台抽象层的便利。实际上,这意味着我愚蠢地为自己设定的限制如下:

  • game must be made entirely from scratch, including the assets

  • all rendering must be done by hand

  • all sound mixing must be done by hand

  • 320x240 target resolution

  • 256 colors only

  • floating point allowed, but behavior must be consistent across platforms

  • decided on fixed point for game logic to guarantee deterministic behavior, floating point for rendering because determinism isn’t that important

  • there must be a finished, polished game that is fun to play (not a tech-demo)

  • platform abstraction layer allowed, but I must pretend it’s very limited (within reason):

    • frame buffer to write pixels into
    • keyboard/mouse input
    • audio buffer to write samples into
    • filesystem I/O
  • no AI slop

  • 游戏必须完全从零开始制作,包括素材

  • 所有渲染必须手动完成

  • 所有声音混合必须手动完成

  • 目标分辨率为 320x240

  • 仅限 256 色

  • 允许使用浮点数,但行为必须在各平台间保持一致

  • 游戏逻辑决定使用定点数以保证确定性行为,渲染则使用浮点数,因为渲染的确定性并不那么重要

  • 必须是一款完成度高、有趣且可玩的游戏(而非技术演示)

  • 允许使用平台抽象层,但我必须假装它非常有限(在合理范围内):

    • 用于写入像素的帧缓冲区
    • 键盘/鼠标输入
    • 用于写入音频样本的音频缓冲区
    • 文件系统 I/O
  • 拒绝 AI 生成的垃圾内容

If this sounds unreasonable to you, that is because it is. But I’m doing it anyway, and today I’m gonna talk about something that is typically overlooked in development blogs, and that is asset creation. 如果你觉得这听起来很不合理,那是因为它确实如此。但我还是决定这么做,今天我将谈谈开发博客中通常被忽视的内容,即素材创作。


Palette Rendering VGA Graphics

调色板渲染 VGA 图形

Mode 13h on VGA hardware was the famous 320x200 256-color graphics mode that defined a generation of PC games. From a programmer’s perspective it was wonderfully simple: you’d have a linear frame buffer where each pixel was represented by a single byte indexing into a palette of 256 colors. If you wanted to draw a pixel, you wrote a byte at a specific address, and that was it, there were no shaders or VRAM, or anything like that. VGA 硬件上的 Mode 13h 是著名的 320x200 256 色图形模式,定义了一代 PC 游戏。从程序员的角度来看,它非常简单:你拥有一个线性帧缓冲区,其中每个像素由一个指向 256 色调色板的字节表示。如果你想绘制一个像素,只需在特定地址写入一个字节即可,没有着色器、显存或其他复杂的东西。

One byte per pixel, and that byte is an index into a palette which contains actual RGB values that would be rendered to screen. This imposes some interesting limitations; when making assets for modern games, you can throw millions of colors at an image, but when your limitation is that every pixel on screen can only be one of 256 colors, asset creation becomes a very different problem because every color choice has to be careful and deliberate. 每个像素占用一个字节,该字节是调色板的索引,调色板中包含将渲染到屏幕上的实际 RGB 值。这带来了一些有趣的限制;在为现代游戏制作素材时,你可以随意使用数百万种颜色,但当限制为屏幕上每个像素只能是 256 种颜色之一时,素材创作就变成了一个完全不同的问题,因为每一次颜色选择都必须谨慎且深思熟虑。

Games like Doom and Duke Nukem are good examples of this done right. There is a certain crispiness and clarity to these graphics that arises because of these technical limitations, not in spite of them. Restriction forces deliberate choices, and deliberate choices tend to look good. 《毁灭战士》(Doom) 和《毁灭公爵》(Duke Nukem) 就是这方面做得很好的例子。这些图形有一种独特的清晰感和锐利感,正是源于这些技术限制,而非克服了它们。限制迫使人们做出深思熟虑的选择,而深思熟虑的选择往往看起来效果更好。

Catlantean 3D is an attempt to reproduce that feeling, but with one caveat - I’m actually going for something closer to VGA Mode-X, which is 320x240. The reason for this is, if you display 320x200 on a 4:3 display, you end up with non-square pixels! While this would be most authentic, I’ve chosen not to deal with this out of preference rather than objective reason. Catlantean 3D 试图重现那种感觉,但有一个前提——我实际上追求的是更接近 VGA Mode-X 的 320x240 分辨率。原因在于,如果你在 4:3 显示器上显示 320x200,最终会得到非正方形的像素!虽然这最符合原汁原味,但我出于个人偏好而非客观原因选择了避开这个问题。


The Palette

调色板

Everything begins with 768 bytes, carefully picked through many iterations of trial and error. The main reasoning for picking these exact colors was the following: 一切始于 768 个字节,经过多次反复试验精心挑选。选择这些特定颜色的主要原因如下:

  • one reserved for transparency (the vibrant pink)

  • one reserved for pure white

  • one reserved for pure black

  • I was obviously going to need a lot of blood, thus reds

  • shades of green and blue because I was going to have red, green and blue keys and color-coded doors

  • game would be set in Catlantis, which is a parody land that resembles ancient Egypt (because cat worship), so obviously, a lot of desert hues (yellows and browns)

  • lots of grays because the setting involves many technical installations (Catlantis is under occupation by cybernetic dog-men)

  • some beige hues to break up monotony over grays, and to serve as warmer replacements when darkening (more on this later)

  • the rest would be filled as necessary when creating textures - highly subjective and impossible to explain, other than “it looked right”

  • 一个预留给透明度(鲜艳的粉红色)

  • 一个预留给纯白色

  • 一个预留给纯黑色

  • 显然需要大量的血迹,因此需要红色

  • 绿色和蓝色的阴影,因为我需要红、绿、蓝钥匙和颜色编码的门

  • 游戏背景设定在 Catlantis,这是一个模仿古埃及的戏仿之地(因为崇拜猫),所以显然需要大量的沙漠色调(黄色和棕色)

  • 大量的灰色,因为场景涉及许多技术设施(Catlantis 正被控制论狗人占领)

  • 一些米色调来打破灰色的单调,并在变暗时作为更温暖的替代品(稍后详述)

  • 其余部分在创建纹理时根据需要填充——这非常主观,无法解释,只能说“看起来很对”。


The Colormap

色彩映射 (Colormap)

Catlantean 3D is a traditional raycaster. The map consists of tiles which are all identical in size; some are walls, others are just voids with a floor and ceiling. In order to render the map, the renderer uses the DDA algorithm for each column of screen, traversing the tilemap and determining where it hits the map geometry, and based on this, a wall column is rendered on screen with the appropriate texture, sampled from appropriate coordinates. Floors and ceilings are rendered after as horizontal scanlines, filling in the rest of the screen. Catlantean 3D 是一款传统的射线投射游戏。地图由大小完全相同的图块组成;有些是墙壁,有些只是带有地板和天花板的空洞。为了渲染地图,渲染器对屏幕的每一列使用 DDA 算法,遍历图块地图并确定它击中地图几何体的位置,据此,在屏幕上渲染出带有适当纹理的墙壁列,并从适当的坐标进行采样。地板和天花板随后作为水平扫描线进行渲染,填充屏幕的其余部分。

Raycasting has been done to death by other blogs and websites, so I’m not going to cover all of it, but I do want to cover what I think is its most overlooked aspect: lighting. 射线投射技术已经被其他博客和网站讲烂了,所以我不会面面俱到,但我确实想谈谈我认为它最被忽视的一个方面:光照。

If we were to render the game world using just the palette, without any special effects, we would end up with something that looked rather flat and unimpressive. But what we wanted was the following. Notice how the light diminishes the further away geometry is from player, and how one side of the map tiles is just slightly darker than the other. This gives an impression of depth. 如果我们仅使用调色板渲染游戏世界,而不加任何特效,最终的效果会显得相当平淡且缺乏吸引力。但我们想要的是这样的效果:注意光线是如何随着几何体远离玩家而减弱的,以及地图图块的一侧是如何比另一侧稍微暗一点的。这给人一种深度感。

With a modern hardware-accelerated renderer, this would be trivially done in a shader - based on how far the vertex is, we would multiply its color vector by a floating point factor and get a diminished color vector as a result. But how do we achieve something like… 在现代硬件加速渲染器中,这在着色器中可以轻松实现——根据顶点距离的远近,我们将颜色向量乘以一个浮点因子,从而得到减弱后的颜色向量。但我们该如何实现类似……的效果呢?