Retro FPS-Style Object Angling Tutorial

AzedaxenAzedaxen Posts: 29Member
edited June 16 in Tutorials

(Sorry for the weird title. As far as I know, there is no common name for this effect!)

Old FPS games like Doom did that thing where the sprites had multiple angles, and the angle shown was dependent on where the sprite was viewed from. Most of these games used 8 angles for the sprites, but I will show you how to write the code in a way that allows you to use any number of angles. I would provide a repo with code, but I'd rather explain how the code works so you can fit it into your game according to your needs.

First, we need to set a variable that stores the number of viewing angles our object will be visible from. I made this a constant, but if you want to make the number of angles different on a per-object basis, make it an export variable instead. I find that multiples of 8 work the best.
const ROTATION_ANGLES := 8

Now, we need to calculate the angle increment and store it as a variable, which is the number of degrees that the viewing angle needs to change by before the object renders from a different angle.
var angle_increment := 360.0/float(ROTATION_ANGLES)

For the next part, I highly recommend getting a reference to the node that represents the object we want to do this effect on and storing it in a variable.
onready var object = $"Insert node path to your object here"

Next, in the _process() function, we need to calculate the viewing angle relative to the camera's position and the object's forward axis. This is calculated with:
var theta = angle_wrap(atan2(target.x, target.z) - atan2(forward.x, forward.z))

  • target is the normalized difference between the camera's position and the object's position. This can be calculated with ((camera.translation - object.translation) * Vector3(1, 0, 1)).normalized() We multiply by Vector3(1, 0, 1) because Retro FPS games did not take the Y-axis of the player into account.
  • forward is the forward axis of the object we are viewing. You can get this from your object with object.get_global_transform().basis.z. This is already normalized, so no need to normalize it yourself.
  • theta will be in radians. angle_wrap() is a function I made that adds 2Pi if theta is less than 0, and subtracts 2Pi if theta is greater than 2Pi.

Now that we have theta, we need to figure out which "angle index" this value corresponds to. An index of 0 represents the angles at which the object should be shown facing the camera. From there, every angle increment going counter-clockwise increases the index by one, up until a maximum of the number of angles minus one. In the case having 8 viewing angles, this would mean that the index ranges from 0 to 7, and increases every 45 degrees.

To find the angle index, I made this function:

func get_angle_index(theta:float) -> int:
    var min_angle := 0.0
    var max_angle := angle_increment * 0.5

    for x in range(ROTATION_ANGLES):
        if theta >= min_angle and theta < max_angle:
            return x
        min_angle = max_angle
        max_angle = min_angle + angle_increment

    return 0

This function loops through all the rotation angles, and checks to see if theta is in between the values that correspond to that angle index, returning that index when it finds that range of values. This function works regardless of whether theta is in degrees or radians, but keep in mind what units theta is in when using the value this returns.

At this point, all we need to do to get the angle index is to use:
var angle_index = get_angle_index(theta)

Now that we have a way to get the angle index, we can use it to display our object from a certain angle! The way this is used is heavily dependent on how your game is set up, so here are some usage examples:

Use with Sprites

You can use Godot's Sprite3D node to place a 2D image in a 3D world. To get the same effect that many modern applications using this effect use, set the Sprite's Billboard property to "Y-Billboard".

If you use sprites, you'll need to have a version of each sprite facing every possible angle index. Early FPS games would often mirror the right-facing sprites to left, reducing the number of sprites that needed to be made.

Use with 3D Models

You can use the angle index to create a psuedo-sprite effect using a 3D model, allowing you to retain the flexibility offered by a model which a sprite does not. I did this by first setting up a scene arranged like this:

  • Enemy (Sprite3D)
    • Viewport
      • Mesh
        • Orbit (Spatial)
          • Camera
    • RemoteTransform

The Sprite3D gets its texture from its child Viewport, which renders a model that cannot be seen by the player's camera. This can be accomplished either by placing the Mesh on its own visual layer, or by setting the Viewport's "Own World" property to true. Enabling Own World, however, will also cause the viewport to use its own lighting!

Orbit is a Spatial node located at Mesh's origin. The camera Orbit is parented to is offset along the positive Z axis. This allows the camera to show the model from a different angle when Orbit is rotated. To rotate the Camera so that it would display the model's angle index, I set Orbit's rotation like this:
orbit.rotation_degrees.y = angle_increment * get_angle_index(theta)

Lastly, the RemoteTransform has its "Remote Path" property set to the Mesh. This is necessary because the mesh being a child of the viewport causes it to not update when the Enemy node is moved or rotated. The RemoteTransform solves this issue.

The end result is that the 3D model will look like a prerendered sprite! You can make this look even more retro by lowering the resolution of the viewport, and by adding shaders to your model.

Edit 1: Fixed typos, added info about using a RemoteTransform for the 3D example.

Comments

  • MegalomaniakMegalomaniak Posts: 2,618Admin

    @Azedaxen said:
    (Sorry for the weird title. As far as I know, there is no common name for this effect!)

    Old FPS games like Doom did that thing where the sprites had multiple angles, and the angle shown was dependent on where the sprite was viewed from. Most of these games used 8 angles for the sprites, but I will show you how to write the code in a way that allows you to use any number of angles. I would provide a repo with code, but I'd rather explain how the code works so you can fit it into your game according to your needs.

    Sounds similar to imposters. I recon it's essentially the same concept.

    http://blog.wolfire.com/2010/10/Imposters

  • cyberealitycybereality Posts: 927Moderator

    Very cool. Thanks for sharing. Yeah, this is the same idea as imposters (the modern version of it anyhow). They even used them for trees in Battlefield 5 and gamers were bugging out. At least in the case of BFV I think they were simply billboards, but I recall some other games used them with different angles, Guitar Hero 2 I think and many sports games for the crowd.

  • MegalomaniakMegalomaniak Posts: 2,618Admin
    edited June 16

    And here's a bit more modern version of imposters/impostors as part of nvidias GPU Gems book:
    https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-21-true-impostors

    In this chapter we present the true impostors method, an efficient technique for adding a large number of simple models to any scene without rendering a large number of polygons. The technique utilizes modern shading hardware to perform ray casting into texture-defined volumes. With this method, multiple depth layers representing non-height-field surface data are associated with quads.

    Like traditional impostors, the true impostors approach rotates the quad around its center so that it always faces the camera. Unlike the traditional impostors technique, which displays a static texture, the true impostors technique uses the pixel shader to ray-cast a view ray through the quad in texture-coordinate space to intersect the 3D model and compute the color at the intersection point. The texture-coordinate space is defined by a frame with the center of the quad as the origin.

    True impostors supports self-shadowing on models, reflections, and refractions, and it is an efficient method for finding distances through volumes.

    Those GPUGems are all worth looking through. Gold mines.

Sign In or Register to comment.