Aspect ratio correction

From ZDoom Wiki
Jump to navigation Jump to search
Zombieman in the game (aspect-corrected) vs the source sprite (appears squished)

Aspect ratio correction (also referred to as Y-stretching, pixel stretching, vertical stretching or pixelratio) is an effect that results in everything you see in Doom — the world geometry, the HUD and the sprites — to be vertically stretched to 120% of its original size. This effect emulates the fact that vanilla Doom ran at the resolution of 320x200, which has a ratio of 16:10, but was rendered on 4:3 monitors that were common at the time; this resulted in the vertical stretch. All vanilla Doom assets were created with this effect in mind: if you view Doom sprites with SLADE, you may notice that the original images appear vertically squished compared to the way they appear in the game (this is especially noticeable on round objects, such as the invisibility power-up or a Cacodemon. (You can learn more about this effect and see examples on The Doom Wiki).

In short, Doom pixels aren't square (by default). This is something that has to be taken into account when creating custom assets, and there are different ways to handle it, depending on the type of asset. If pixel stretching is ignored, it results in assets that look visibly stretched, different from what the author intended. One common example is things like scoped weapon sprites (or other circular elements), where the scope will appear as an ellipse instead of a circle unless the sprite was initially made with pixel stretching in mind.

Note on the values

The pixels and the world in Doom are rendered 20% taller than the original values are. This means that a 64x64 sprite by default appears stretched as if it was 64x76.8 pixels (64 * 1.2 = 76.8). If you want to scale a sprite down, its vertical resolution has to be reduced to 83.333...% of the original value (not 80%!). Of course you'll have to use the closest real value, such as 83.33334 (e.g. 76.8 * 0.833334 = 64.0000512).

Note, you're not actually required to make assets this way, and the actual method of approaching vertical stretching is different depending on the type of asset; this is just a general reminder on how percentages work.

Viewport

Everything you see on the screen is vertically stretched by default. Taking world geometry as an example, if you create a room 128x128x128 map units, it'll appear as if it's taller than it is wide instead of looking like a proper cube. The same effect applies to actor sprites, which are stretched by default.

This effect can be easily controlled by the author via MAPINFO by using the pixelratio option. The default value is 1.2, which makes the world stretched; using 1.0 will make it square. This option is a simple post-processing effect that applies to the screen, it doesn't actually change any interactions in the game.

However, pixelratio doesn't affect everything you see:

  • Sprites drawn on the screen by weapons and CustomInventory, as well as HUD are unaffected by pixelratio, they need to be handled separately.
  • 3D models are always shorter than the rest of the world, so they need special handling.

These cases are covered below.

Actor sprites

Actor sprites can be scaled separately from the world, by using scale, scaleX and scaleY actor properties. Note, these values are added on top of the viewport scaling defined with pixelratio.

If you're making a project that has its own maps and sprites created specifically for those maps, you can simply define the desired pixelratio and forget about it (pixelratio 1.0 will make sure actors are rendered square, just like the world). For a fully stanalone project (such as a TC or a full game) nowadays there's not much reason to stick to the Doom pixelratio of 1.2.

However, if you're making custom sprites for a mod that is meant to work with vanilla levels or otherwise work with pixelratio 1.2, you'll have to vertically scale the sprites in such a way that they don't actually appear as stretched, since the world will be subject to the default vertical stretching. There are 2 primary ways to do it:

  • Use scaleY 0.833334 in the properties of every actor. (Normally values like 0.834 or just 0.83 will be close enough that the players normally won't notice.) This will counteract the vertical stretching applied to the world, making sure the actor's sprite is rendered the same way you see in in SLADE or your graphics software.
  • Initially create your asset to be squished, like vanilla Doom assets were, so that pixel stretching renders them to the correct size. For example, if you want to create a sprite that appears as a 64x64 square, you need to make it around 64x53 pixels (53 * 1.2 = 63.6 — close enough).
    • Note, from a technical perspective this is naturally a more difficult method, since you will have to actually draw the sprite in a squished form. You could draw the sprite normally and then scale it down vertically in a graphics software, but downscaling at low resolution usually causes significant distortions or artifacts in the graphics, so this is not recommended.
    • Some graphics editing programs allow defining a custom pixel ratio, so that you can create a ratio of 1:1.2, which will make the images you're editing appear already stretched in real-time, the way they're rendered in GZDoom. In Photoshop this can be done by navigating to View > Pixel Aspect Ratio > Custom Pixel Aspect Ratio. Aesprite also allows creating and importing custom pixel ratio, as described here.

On-screen sprites

Sprites drawn on the screen by weapons or CustomInventory (which are the only two classes that can draw on the screen) are handled entirely separately from the viewport and are unaffected by pixelratio. The scale properties also don't affect on-screen sprites, since they can only change the appearance of the pickup (i.e. the sprites defined in the Spawn state sequence for that weapon/item).

There are several ways to scale the sprites meant to be drawn on the screen:

  • The simplest way is to use the Weapon.WeaponScaleY property (New from 4.8.0). The default value is 1.2 (stretched like the world), so you simply need to use the value of 1.0 to make the sprites appear the way the original graphic looks.
  • Use the graphic as a patch (by putting it in the patches/ folder in your PK3) and add it to the TEXTURES lump (in SLADE: select the images, right click, choose Graphic > Add to TEXTUREx). After that double-click the TEXTURES lump in SLADE to open the visual TEXTURES editor, and make sure you tick the Apply Scale in the top panel to the right; then set Offset Type in the bottom left to HUD to view the sprite the way it will appear in the game. After that navigate to the Scale section below and set the second value to 1.2. Note, TEXTURES uses inverse scale, so setting the Y scale to 1.2 will scale it down to the exact value you want (while using numbers below 1.0 will make it larger). Don't forget to define the sprite as a sprite by setting the Type field to Sprite, because patches added to TEXTURES are added as textures by default, which means the game won't see them as sprites.
    • The only downside of this method is that it's cumbersome to perform for multiple sprites. Technically you can edit the values on only one sprite, then open the TEXTURES lump in a text editor, like Notepad++, and just copy-paste the values to other sprites (and find & replace "texture" with "sprite" to make sure they're in the correct namespace), but it can still be a slow process.
  • You can also use A_OverlayScale (GZDoom only) to scale any sprite layer to the desired value (for example, A_OverlayScale(OverlayID(), 1, 0.83334) will scale the calling layer to be square), but you have to remember to call it at the beginning of every state sequence, and also take it into account if you're using this function in other places to scale the sprite, so it's also not always convenient.
  • Finally, you can initially create your sprites to be squished, so that they get scaled to their proper size in the game, as described above.

HUD graphics

Images drawn by the HUD/statusbar (as well as other UI elements, such as menus) are also unaffected by pixelratio and are scaled separately from the world or on-screen sprites drawn by Weapon/CustomInventory. Every UI function has its own arguments for scaling, but talking about HUDs specifically, there are several aspects at play:

  • User interface scale: This is an option that lets the user adjust the visual size of the HUD however they like. It can be found in GZDoom under Options > HUD Options > Scaling Options. The default value is "Adapt to screen size," meaning the HUD is upscaled or downscaled to match the screen resolution, while the other values can make the HUD bigger or smaller. This option doesn't affect vertical stretching, it simply defines the size of the HUD elements, so that the players can make the HUD comfortable for them.
  • Aspect ratio: Also found under Options > HUD Options > Scaling Options (can also be changed with the hud_aspectscale console variable), this option actually controls whether the HUD is vertically stretched or not. The default value is 1.2 (stretch like in vanilla Doom), but in contrast to all the cases above, this is actually fully under the player's control and can't be enforced.

If your HUD has elements that look visibly bad when subjected to pixel stretching (such as clearly defined circles or squares), HUD aspect ratio may become a problem for you. Currently there are only 2 ways you can work around it:

  • Make your HUD forcescaled. This can be done by calling BeginHUD() with the forcescaled argument set to true in your ZScript HUD (see BaseStatusBar), or by using the forcescaled flag in in your SBARINFO StatusBar. A forcescaled HUD ignores HUD aspect ratio and always draws the images exactly as you define them.
    • The problem with this method is that it also ignores the user interface scale option, which means the players won't be able to adjust the size of the HUD to their liking.
  • (ZScript only) You can create your own drawing functions for your ZScript HUD that will check the value of the hud_aspectscale CVAR and conditionally multiply every element's vertical scale and position (since it's affected by scale) by 0.83334%, so that they're automatically downscaled if the CVAR is true, effectively remaining unaffected by the HUD aspect ratio setting.

Here's an example of how to create a wrapper for the DrawImage() function that will do exactly that:

class MyCustomHUD : BaseStatusBar 
{
	const noYStretch = 0.833334; //anti-stretching factor
	transient CVar aspectScale; //this will cache the CVAR value
	
	// This is a verstion of DrawImage() that automatically adjusts
	// the graphic's position and scale to effectively ignore
	// the value of the hud_aspectscale CVAR:
	void NoAspectDrawImage(String texture, Vector2 pos, int flags = 0, double Alpha = 1., Vector2 box = (-1, -1), Vector2 scale = (1, 1)) 
	{
		// Cache the CVar value to a local variable to avoid 
		// calling GetCVar() every time, since that can be
		// resource-intensive (once it's cached, this block
		// won't be called again):
		if (aspectScale == null)
		{
			aspectScale = CVar.GetCvar('hud_aspectscale',CPlayer);
		}
		if (aspectScale.GetBool() == true) 
		{
			scale.y *= noYStretch;
			pos.y *= noYStretch;
		}
		DrawImage(texture, pos, flags, Alpha, box, scale);
	}
}

Once you've done this, you'll simply need to call NoAspectDrawImage instead of the regular DrawImage to draw in your HUD.

Notes:

  • It's always recommended to cache CVARs instead of continuously calling CVar.GetCVar(); that function isn't particularly cheap.
  • The variable that caches the CVar has to be defined as transient because it shouldn't be written into save games.

3D Models

Models are in a peculiar place in contrast to other assets: they're still affected by pixelratio, like everything in the viewport, but they're also hardcoded to appear shorter than everything else. This means that if you create a 64x64x64 cube of world geometry and place a 64x64x64 3D model next to it, the model will always be shorter, regardless of pixelratio. With the default pixelratio of 1.2 the models will be rendered with properly cubic polygons (as opposed to stretched pixels on everything else), while with pixelratio of 1.0 the polygons will be squished, and so on.

If you're creating a mod with models without maps, and you just want to have evenly-sized 3D models, you can normally use them as-is. However, if you want the size of the models to match the world geometry, you need to multiply the model's Z scale by 1.2 in MODELDEF to make sure the model is stretched appropriately regardless of the pixelratio value (for example, Scale 1 1 1.2 will work for models that don't need to be otherwise scaled). This can be especially important in cases when models use the same textures as the map and are meant to represent complex geometry, such as archways, windows, and so on.

Note:

  • All of the above is true only for the models placed in the world. First-person models used by weapons can be scaled the same way as weapon sprites: with WeaponScaleY and A_OverlayScale.
  • When exporting world geometry as models from Ultimate Doom Builder, the models' Z scale will be automatically multiplied by 1.2 to match the world.