体素位移渲染器 – 复古 3D 美学的现代化
Voxel Displacement Renderer – Modernizing the Retro 3D Aesthetic

原始链接: https://blog.danielschroeder.me/2024/05/voxel-displacement-modernizing-retro-3d/

在这篇博文中,作者讨论了自定义实时渲染器的开发,该渲染器利用微小的体素来重新创建 90 年代 3D 游戏的独特视觉风格。 渲染器不是使用传统方法,而是从常见的艺术资源(低多边形三角形网格和纹理)生成这些样式。 主要目标是将 90 年代的复古 3D 游戏体验与当代美学融为一体。 作者在开始旅程时问道:“如果我们在经典 3D 环境中的纹理中添加体素大小的表面细节而不是 2D 像素会怎样?” 他们通过结合预处理纹理和精确的位移映射技术,将传统的三角形网格转换为详细的体素模型,从而实现了这种独特的混合。 渲染过程涉及将三角形网格、纹理数据和位移图转换为 GPU 兼容的格式,从而以最小的延迟实现平滑渲染。 纹理经过预处理以获得额外的必要信息。 然后,GPU 接管,生成最终的体素详细输出。 为了创建艺术资产,设计师为每个元素开发纹理(反照率和伴随的位移)。 保持与复古风格视觉效果的一致性至关重要。 三角形网格保持原样,作为基础结构,由于位移贴图而出现更精细的细节。 总体而言,该技术使开发人员能够使用熟悉的工具在新框架内制作内容。 好处包括保持传统工具的易用性、增强的多功能性以及与现有游戏引擎的潜在集成。 未来的计划包括实现对移动物体的支持、添加更复杂的照明条件以及增强性能等进步。 请继续关注更新!

这篇文章承认了对经典《毁灭战士》游戏的体素《毁灭战士》修改,重点是通过体素化地形而不是怪物设计来增强环境。 2022 年末,其创建者开始探索视差映射,将更详细的体素特征融入关卡几何形状中。 虽然由于视差贴图的固有局限性导致实现未能达到预期,但该模组的成功激发了作者项目开发的热情。 虽然该方法允许使用动画元素(例如门),但作者提到了使用壳映射时更尖锐的弯曲区域面临的挑战。 相反,现代游戏主要利用置换贴图,通过在每个像素的基础上操纵片段着色器内的顶点位置来实现复杂的细节近似。 尽管最初被误解,但该技术涉及将位移贴图渲染到大三角形表面上,模拟密集网格。 考虑到所需的高度并行处理,引擎通常采用 GPU 来实现最佳性能。 尽管取得了这些进步,但文章的作者更喜欢更简单的方法,创造出独特的复古美学。
相关文章

原文

This is the first of two posts about a renderer I have been developing, focusing on what I’ve built and how it works. The second post discusses why I’ve built it and my plans for the future.

I’ve been developing a custom real-time renderer which uses very small voxels to produce a distinctive visual style that modernizes the look and feel of classic 90’s 3D games. By approaching the problem of rendering voxels in an unusual way, I’m able to produce these visuals from conventional art assets — low-poly triangle meshes and textures — that are familiar and efficient to create.

Before going further, a video is worth a thousand words. (Set quality to 1440p and view full-screen for best results.)

Read on for more information about the chosen visual style, how I approach it at a technical level, what this means for creating art assets and environments, and where I’m going from here.

Aesthetic motivation

Early-to-mid 90’s 3D games like Doom, Quake, and Duke Nukem were immensely influential for their technology and gameplay. More recently, the limitations of their visuals have taken on a retro appeal. Environment geometry was simple and imaginative, and textures were displayed with sharp pixel boundaries. I see this as 3D’s counterpart to the stylized abstraction of 2D pixel art. This has become a popular aesthetic for modern retro-inspired games, and 3D indie titles more broadly.

BTSX Episode 2, a community-made level set for Doom 2 featuring custom textures.

I started this work with a simple question: what if, instead of blocky two-dimensional details, the surfaces in these environments had blocky three-dimensional details — voxels, instead of pixels? For example, a cobblestone wall could pop with geometric depth while retaining a pixelated look when viewed up close.

As I fleshed out this idea and built the renderer you see in the video, I aimed for the visuals to have a foot in both worlds between the look of 90’s games and something more modern. To put it another way: this is what Doom looked like to me when I was young.

How voxels usually work

Before getting into how I model and render this voxel aesthetic, it’s worth reviewing how voxels are typically used in games, and some of the consequences this has. (Or, skip ahead to “what I’ve built.”)

A voxel mesh is a three-dimensional grid where every cube-shaped cell is filled in or empty. These cubes may be large and textured, like in Minecraft, or small and solid-colored, like in Teardown1. To build a large environment with voxels, you can place all the geometry on a shared grid (Minecraft), or position many independent voxel meshes into the same space (Teardown). Regardless, once you decide your game world will be formed with voxels, you have to create this voxel geometry, and your rendering and gameplay logic needs to operate in terms of voxels.

Big voxels, little voxels. (Minecraft, Teardown.)

Software exists to author voxel meshes directly, but the process is time-consuming and often limited to creating smaller meshes. You can create larger environments by modeling building blocks, like tileable sections of wall or floor, and assembling them into larger structures. This works well for grid-aligned buildings but poorly for free-flowing terrain.

Many voxel games instead opt for procedural generation, which works great for natural environments and can be used to combine building blocks like the ones mentioned above automatically. But procedural generation brings its own headaches, and is only suitable for certain kinds of games.

It’s also possible to create voxel geometry by modeling the object or terrain that you want as a conventional triangle mesh and then rasterizing it to a voxel grid. This is a powerful technique, though it does mean that all the resulting geometry occupies a single voxel grid, which may not be desirable aesthetically. For example, if one face in the triangle mesh is axis-aligned and another is at a 45-degree angle, the first face will become a flat slab of voxels, while the second will look like a staircase.

Whatever method you use to create your voxel geometry, because your scene is canonically represented in terms of voxels, your rendering and game logic need to operate at the voxel level. Especially if the voxels are small, this may mean having to implement your own physics, character controller, NPC pathfinding, etc., as well as rendering.

On the plus side, it’s easy to add or remove geometry from voxel meshes; games like Minecraft and Teardown take full advantage of this strength.

How displacement usually works

In my case, I simply want to add voxel-like surface detailing to the kinds of environments found in classic 3D games. Those environments consist of largely static, texture-mapped triangle meshes. So what about solving the problem with displacement mapping?

For folks who aren’t familiar with it, displacement mapping pairs your textures with displacement maps that define how far each pixel of the texture should extend inwards or outwards from the surface of the mesh. This can be a very fast way for artists to add detail to geometry.

3D modeling software can apply displacement maps to geometry by subdividing the mesh and then displacing the new vertices, so the displacement from the texture becomes real geometry, even affecting the silhouette of the object. The resulting meshes have radically higher polygon counts, however, and input meshes that have hard edges or corners must be modified to produce good results.

An example of displacement mapping as used in 3D modeling, showing an albedo map (top left), a corresponding displacement map (top right), and a mesh that has been subdivided and displaced accordingly (bottom, showing triangle edges on the right). Material by Dimitrios Savva, CC0 license.

For real-time graphics, displacement maps are sometimes used with surface shading effects like parallax occlusion mapping to create the appearance of fine geometric details on meshes without actually changing their physical shape. These effects can look good on large floors and walls, but the illusion breaks down along the edges of objects, where the geometry remains flat2.

An example of parallax occlusion mapping, showing a mesh before and after applying the effect. The edges of the object are unaffected by the apparent 3D detailing. Screenshots are from a babylon.js demo.

A method known as shell mapping provides something of a middle ground between the two approaches above, making it possible to see displacement even along the silhouette of an object without having to form a highly subdivided mesh. However, it struggles with areas where the input mesh has high curvature3; this makes it hard to adapt input meshes that have hard edges or corners for use with the technique.

What I’ve built — mixing voxels and displacement mapping

For my purposes, I wanted to use conventional low-poly meshes to model environments like those of classic 3D games, apply displacement maps to define voxel-scale surface details, and render a result that truly looks like it was built from voxels. These environments are full of sharp edges, like the corner of a building. Conventional displacement mapping already struggles with these regions; in my case, I also wanted the results to look like voxels.

A section of the demo environment before and after displacement. The brown stone arches are created as triangle meshes and then displaced, but the results look like they are assembled from voxels, even along the edges of the original mesh.

Solving this problem was very, very hard, and involved building a lot of custom machinery. (For now, this is a standalone C++ / Vulkan project; more about that below.) This infrastructure performs three main tasks:

  • Taking the triangle meshes that define the environment, plus limited information about the displacement maps, and converting them into the geometry data that the renderer will load onto the GPU to draw the displaced versions of the meshes4. This process runs on the CPU before any frames have been rendered. In a shipped game, you could bake the results to disk, though this isn’t too expensive; for the demo environment in the video, converting the entire scene on a single thread takes half a second.
  • Pre-processing the textures to generate other information needed during rendering, such as normal maps. This work is pretty basic, but it is slow, so you’d definitely bake the results.
  • Using the mesh and texture state generated above to draw the voxel-displacement geometry. This happens on the GPU with almost no CPU involvement per frame.

While there’s room for further optimization, it already performs well: on a Radeon RX 5700 XT (which was considered a 1440p card when it launched in 2019), the demo sequence seen in the YouTube video renders at 1440p with frame times from 4 to 9 ms (250 to 110 FPS). On a Steam Deck OLED at its native 800p, the sequence stays above 60 FPS, with many areas locked at 90.

Creating art assets and environments

So, the tech exists; what does it look like to make content for it? There are two kinds of assets to create: textures and meshes.

I author each texture as an albedo map and a displacement map. For the albedo, I’ve found that imitating the limited palette of retro games helps to sell the visual style. The displacement maps look very standard, providing fine-grained height values that you can rescale in the renderer to make the displacement more or less intense. In this case, the units for the rescaled height values are how many voxels inward or outward to move the surface. The renderer only displaces to the nearest whole number of voxels, but it uses the exact heights to calculate a normal map, so that features less than a voxel tall can still affect the lighting.

Albedo and displacement maps for the gray stone block material, and the rendered result. Large changes in displacement become voxel-scale geometry; subtler changes, like the ridges on the surface of each stone, may not become voxels but do affect how the surface is lit.

Next are the meshes. These are conventional low-poly texture-mapped triangle meshes with shading normals. The normals indicate which sections of the mesh represent smoothly curving surfaces and which edges are supposed to be sharp; as mentioned above, my renderer has to do extra work for sharp edges to produce good-looking results. For the sake of the visual style, I try to keep the size of the voxels consistent when mapping the textures, but this isn’t a hard requirement.

A cave, as modeled in Blender and in my renderer. I modeled the area as two meshes — the cave walls, and the floor. The entrance on the left has hard edges, as visible in the Blender screenshot. In the renderer, these edges become detailed according to the displacement of the stone texture.

There are limitations on how you structure the meshes and how you map the textures to them. Some of these limitations I can remove in the future, and others are unavoidable because of how the renderer works5. That said, because these are triangle meshes, not some purpose-built geometric representation, it’s possible to use many different tools to create them. I’ve used Blender to model my demo environment, but to illustrate the point, the current export format from Blender to my demo is literally just an OBJ file.

Benefits of this approach

Approaching the voxel aesthetic in this way has some noteworthy benefits. As described above, you get to use a familiar workflow to create content, rather than having to use specialized voxel-editing software. Authoring good textures can be challenging (at least for me), but once you’ve done so, they’re easy to reuse across a range of geometry without having to do further voxel-scale work. For example, a large fraction of the demo geometry in the video uses a single texture (the stone blocks, in three different colors).

So far, what I’ve been building is just a demo of a renderer. But some of the most interesting consequences of solving the problem this way go beyond rendering to the other components of a game. Because the environment is authored as triangle meshes, with the voxel features being only surface decoration, you can treat the environment as being polygonal geometry whenever it’s convenient to do so.

In my demo application, you can move around the environment from a first-person perspective with full collision, walking up stairs and being blocked by walls, but I didn’t have to write my own physics engine or character controller; I integrated an open-source physics library and simply used the original triangle meshes as the collision geometry. Similarly, problems like enemy movement and pathing could be solved in terms of the same meshes. Only select gameplay systems would need to care about the voxels; for example, in a first person shooter, you could implement a displacement-aware raycast function so that bullets can respect the exact shapes of the displaced geometry.

Many voxel-based games use their own engines, because so many gameplay systems have to be customized. This project is currently a standalone codebase using C++ and Vulkan, but as I’ve argued, most of the logic outside of rendering doesn’t need to know or care about the voxel-scale details. As a result, I think the most realistic path to using this rendering technique in a game would be to integrate it into an existing engine to benefit from all the other provided systems. But that’s getting into the topic of the next post.

Where this goes from here

So, where am I going from here?

For the renderer itself, there are several features I’d like to add or improve. What I’ve built so far is a great way to create level geometry, but game environments also feature smaller objects, decorations, and dynamic elements like enemies. I have ideas for how to integrate smaller or animated objects into this art style in the future. While the lighting in the demo already looks good, the current implementation is limited to a small number of lights, and lacks shadows, ambient occlusion, or other fancier features. Because this renderer is meant for the largely static environments that most games have, rather than the highly dynamic geometry of typical voxel games, there’s a lot of flexibility to handle lighting in different ways (including baked approaches), depending on the needs of a particular application. The renderer currently has no anti-aliasing, which becomes noticeable on distant floors in particular; there are some interesting possibilities for dealing with this.

Zooming out further: where does this project go? What am I planning to do with it? For more on that, especially if you’re a developer or studio who finds this work intriguing, please see the second post. And thank you for reading!

Three different textures applied to the same input mesh, a twisted pillar. The inset shows the input mesh, with the sharp edges in black.
联系我们 contact @ memedata.com