16-Bit+ Style Retro 3D (fantasy console-ish)

I’m developing a pixel-y 3D style that draws inspiration from the late 1980s to early 1990s, following certain rules to imitate hardware limitations and promote an economical workflow for a stubborn solo dev, while taking advantage of some more modern techniques. I began the project while I was working in Godot Engine, after learning to write shaders. I’ve been rewriting everything for Defold now, including a personal render script that implements several needed features.

Defold seems uniquely well-suited to pursue a 3D aesthetic like this one, because as of late it offers some essential 3D features, still without making many assumptions.

I want to use this thread to share what I’m working on, and to leave some context for what it means.

RULES

  • Low resolution viewport – like Derez
  • Low polygon budget per model
  • Mesh faces are mostly-strictly a single shade or pattern per triangle
  • Certain values are quantized; lighting/shading is posterized and dithered
  • Limited color blending and “palettes” (without actually inhibiting overall color depth)
  • Affine texture mapping, low resolution, unfiltered
  • Vertices snap to pixels
  • Line or point* primitives are preferred whenever suitable – like Desolve
  • 3D transparency is “screen door”/“checkerboard” style

* - Points are not offered in Defold, but I found a workaround

First, here are a couple clips (from Godot) that help exhibit these rules put into practice:


7 Likes

In Defold, since learning how to implement shadow mapping, I extended it to particle occlusion, as I have done before in Godot. I also like how this vertex shader turned out for weather effects; this splash effect was a new idea.

Perfect planar reflections are a nice reward for going old-school in style and design.


10 Likes

I’m excited to follow this dev diary! I really like the self-imposed rules you have! :+1:

3 Likes

I’m getting more comfortable with the nuances of Lua and how Defold utilizes it, so I’m feeling out a favorable way to re-implement point-based lights and spotlights. Fortunately, I’ve done it before, so I don’t feel put out writing them from scratch. :grinning: Part of the puzzle has been figuring out the best way to bend modern dynamic lights into the aesthetic without breaking it, or creating a mess of excess dithering.

Something that looks fancy, but inhibited, will do. Art > Authenticity. I think I’m satisfied with blending a few shades. I’ve drawn inspiration from the lantern in The Legend of Zelda: A Link to the Past:
zelda_dungeon1

Also on display: like I mentioned in the first post, I found a workaround for point primitive lookalikes, by using a vertex shader to draw lines that are always barely 1px long. The stars are a cloud of vertices in a mesh component. :slightly_smiling_face: It’s a shame the point primitive has been forgotten with time, when it is still so useful. For Defold, I submitted issue #8701 about it. :pray:

9 Likes

Looks great!

1 Like

Good. Thanks! That’s more promising than the reaction I got to a dithered version of a similar scene. :sweat_smile: It didn’t make much sense to blend several dithered light sources; blending and dithering compete for the same roles. Full-color blending breaks the illusion, and binary lighting seemed too harsh.

1 Like

I found the time to put more work into the point/spotlights, and made a little festive video.

4 Likes

I’ll explain the gist of the rendering loop, for the purpose of example.

The first draw call is for the stars, because they’re furthest away – except not really; it’s a 1m-radius ball rotated by a globe matrix and the camera. Then the background layer is drawn with a shader I developed; the sun, moon, sky simulation, and surface plane. After that, 3D meshes are drawn on top.

There are two main kinds of 3D meshes; smaller meshes, called objects, and much larger meshes, called the scene. Objects cast shadows but don’t receive them; the scene receives shadows but does not cast them. This avoids many shadow artifacts, and suits the period aesthetic. Win-win. :+1: Objects and the scene interact with lights a bit differently; objects take advantage of their locality to conform to the shade-per-face rule.

The background interacts with lights on its own, and has the help of an invisible mesh to catch shadows at the surface plane. The visuals maintain context even without 3D models to support it, obviating the need for a base mesh like a heightmap.

Reflections are an extra component & material rendered in a separate draw call, upside-down and inside-out (render.FACE_FRONT), and optimized with Y-inverted frustum culling. :slightly_smiling_face: Reflections are discarded, or not, depending on whether they should be visible “under” the background plane. They are wasteful to include if there is no water nearby.

Lighting from the sun is scattered by the atmosphere, and ambient light changes color/intensity based on the current environment. I’ve included basic material properties – analogous to “metallic” and “roughness” – to make things shiny or reflective, or to make surfaces glow. Those are stored in vertex colors, so models still only need one material.

I’ve been devising this 3D rendering environment for a couple years, as I learn shaders and 3D programming. I’m pleased with how it’s going in Defold now. :defold:

2 Likes

Thank you for describing the setup. Pretty cool!

I’ve begun creating a custom rigidbody collision solver for kinematic type objects. At the very least, it would be useful to have a dynamic-like collision object with the ability to customize the inertia tensor. But also, I can apply forces & torques one frame sooner than the message system, by changing velocities directly. :clock2:

If I can get all the wrinkles ironed out, it could be a nifty module to replace a dynamic type object when you need more control over it. :wolf: So far I’m just happy that a box tumbles in the correct direction without jumping into outer space.

3 Likes

Hmm…I don’t think I will be able to make a complex dynamic body replacement with a Lua script. There is no good way to iteratively solve collisions, then. All I need, at minimum, is decent collisions for an object that would spend most of its time suspended above the ground, with a custom inertia tensor. I might still be able to accomplish that.

But I think I should submit some feature requests. :disappointed_relieved:

The basic example in the docs works perfectly and smoothly in 3D as well as 2D, but does not resolve rotation from collisions. Doing that in a stable way is tricky. With just the basics, it’s flawless:

1 Like