[DevLog] #05 Considerations about pixel-perfect graphics

Frostory

Embark on an enchanting journey with the little fairy, Sir Leonardo, through a mysterious world. Meet companions, battle wicked foes, and uncover the secrets of a perilous realm in an adventure that awaits. The ending of the story lies in your hands.

[i][Translated from Korean][/i] Hello! This is Team OOPArts, currently developing Frostory. This time, we’d like to discuss the considerations we had about pixel-perfect rendering in the early stages of development. [img]{STEAM_CLAN_IMAGE}/44884140/f34265365efa943acc8feb149a3416052756bcf1.png[/img] At the beginning of development, we didn’t pay much attention to pixel-perfect rendering. (In fact, we didn’t even know the term "pixel-perfect" back then.) However, as development progressed, we came across the concept of pixel-perfect rendering. We found that various approaches were used in different games. [img]{STEAM_CLAN_IMAGE}/44884140/a37682fc39bb9464bf7f2a7a2d435ced097d7706.png[/img] Perfect Pixel-Perfect Rendering [img]{STEAM_CLAN_IMAGE}/44884140/a6f4a489496402952b75ffb5dafe287b1b84e975.png[/img] When Pixel Snap is not applied (slight misalignment between background and character) [img]{STEAM_CLAN_IMAGE}/44884140/abff3329227e2118ecbc13766bc20b05c6ef1e87.png[/img] Allowing pixel rotation and scaling Each approach has its own advantages and disadvantages. The first method offers the best presentation when stationary and has a strong retro feel. [img]{STEAM_CLAN_IMAGE}/44884140/5b9e78e43e863c4dd8971f5fa05946dccfd5b335.gif[/img] However, when forced to render rotating objects in pixel-perfect units, the pixels looked broken, which felt uncomfortable. (There’s a glitch in the character rendering in the image on the right, but please focus on the fruit instead.) Depending on the art style and preference, one may choose differently, but we concluded that the left method was better for our game. There were various ways to apply pixel snap, each with its own characteristics. 1. Pixel Snapping at the Rendering Stage [img]{STEAM_CLAN_IMAGE}/44884140/e5626c35412959bcd8432476afb48a13bc71b887.gif[/img] (Rendering at a small texture size and then scaling, or rounding all sprite coordinates to the nearest pixel) - Perfect pixel-perfect rendering, but camera movement is also pixel-locked - Objects moving slowly may look awkward - Suitable for retro-style or fixed-resolution games 2. Objects Already Snapped (All object coordinates are pixel-locked) [img]{STEAM_CLAN_IMAGE}/44884140/8a71ae9d5a014f9f774ad0de97649fe053543c2e.gif[/img] - Allows the camera to move smoothly in screen pixel units - Difficult to implement physics 3. No Snapping [img]{STEAM_CLAN_IMAGE}/44884140/f1d2bb091de88cf2d2585ffa25809584c9706840.gif[/img] - Smooth movement for both camera and objects - Potential for glitching between objects at pixel level - Stationary objects may appear to shift by one pixel as the camera moves (Look closely at the left tree) After considering the pros and cons, we decided not to apply pixel snapping. The calculations for movement in pixel units are complex, and the smooth motion felt acceptable. After making this decision, we realized that stationary objects have no physical movement, so we snapped only stationary objects to the background pixel position to fix issues like the left tree. Although we made some decisions, there were still issues. [img]{STEAM_CLAN_IMAGE}/44884140/dc96a67de06779be8b6c1d00e1fec03a72e6af0b.png[/img] When learning about pixel-perfect rendering, we referenced Enter the Gungeon extensively. The main focus was the display options, which offered a Scaling Mode for players to choose their preferred graphics. The reason for these options is likely that people use monitors of various sizes, making full-screen pixel-perfect rendering impossible at all times. By comparing pros and cons, they allowed users to make a choice. These options can be categorized as follows: 1. When the monitor has an integer-multiple resolution of Enter the Gungeon's art pixels (480x270), like 1920x1080, 1440x810, etc. Setting it to the monitor resolution works perfectly. 2. When it’s not the case - If pixel-perfect is desired, adjust the output range to make it pixel-perfect (this will add letterboxing). - If you want a full-screen view, scale the game scene and choose whether to apply anti-aliasing. Understanding why these options are provided requires knowing the following concepts. [Game Scene] [img]{STEAM_CLAN_IMAGE}/44884140/3156e60c7617356b0c1860f5cba2d32e2ff33877.png[/img] The game scene you want to display. In the case of Enter the Gungeon, it’s 480x270, while for our game, it’s set at 320x180. (Currently, it has been changed to 427x240) The technique of scaling this beautifully is crucial. [PPU] [img]{STEAM_CLAN_IMAGE}/44884140/2d8307c222086d2c22ae9e23044961156343d759.png[/img] Sprites ultimately exist within the game world. PPU, or Pixels Per Unit, determines the size of one sprite pixel in the world. Unity’s default is 100 PPU, meaning 100 pixels equal a length of 1 unit. This value varies depending on the game’s requirements. We use 36 PPU to set one tile with a width of 18 pixels as 0.5 in size. [Camera Orthogonal Size] [img]{STEAM_CLAN_IMAGE}/44884140/4ddbc2fd2275b905fede0ad9289e05b03f054d0f.png[/img] The orthogonal size is the height of the world captured by the camera in the y-axis. The camera automatically adjusts its x-axis according to the screen ratio. You need to calculate this orthogonal size correctly to fit the world precisely. To fit by y-axis, orthogonal size = H / PPU, and to fit by x-axis, orthogonal size = W / PPU * screenHeight / screenWidth, where W is the game scene’s width and H is its height. What if you want the game scene to be displayed proportionally regardless of screen size? In this case, you can add letterboxing by adjusting the camera's rect value. The camera’s two corners min and max default to (0,0) and (1,1). For example, if the screen ratio is 4:3 and the game scene ratio is 16:9, you can set it to (0, 0.125) and (1, 0.875) for a perfect fit. [Camera Back Buffer] [img]{STEAM_CLAN_IMAGE}/44884140/57a4d2ab33ac10ee9478aa55a91f445a5278c75e.png[/img] The camera renders the world onto a render texture known as the Back Buffer (framebuffer). Therefore, if the Back Buffer size matches the game scene size, pixel-perfect rendering is achieved (putting other issues aside for now). What if the Back Buffer size is twice the game scene size? Even in this case, pixel-perfect rendering is achieved. (See image) What if the size is 1.5 times? The pixels will appear distorted. This happens because each pixel can only store one color. As a result, during scaling by the rasterizer, pixels may seem to jump irregularly. (Mathematically, the pixels are scaled according to a formula, but complex pixel art often appears distorted.) To understand this intuitively, you can recreate similar situations with a graphics program. The image distorts until it scales perfectly to twice the size. [img]{STEAM_CLAN_IMAGE}/44884140/cab84af4ca2bda92c196483e34be2a98ec0be186.gif[/img] In summary, the Back Buffer size should be an integer multiple of the game scene size. In Unity, you can set the size using the Screen.SetResolution function. [Screen Pixels] The pixel information in the Back Buffer is displayed through the monitor. Monitors are made up of pixels at their native resolution (1920x1080, 2560x1440, etc.). If you want a perfectly clear pixel art look, this also needs to be an integer multiple. However, as everyone uses monitors with different resolutions, there are various practical issues. It seems better to scale and apply anti-aliasing when the Back Buffer doesn’t have an integer multiple on the screen. In general, monitor resolutions are much higher than the game scene size, so even with anti-aliasing, the pixel art look can be preserved. In this case, even without anti-aliasing, the visuals aren’t heavily affected, so it seems to be a matter of choice. [img]{STEAM_CLAN_IMAGE}/44884140/1c731146c712da81922773662b6fa24e646f3db0.png[/img] In the left example above, you can see where the screen pixels misalign. It generally looks fine, but if you look closely at the diagonal lines, the pixel sizes become irregular. [img]{STEAM_CLAN_IMAGE}/44884140/ca4baf12fc0a0b3f4fbc6b0347e6b3d37230e007.png[/img] Ultimately, we designed our UI with these considerations in mind. Unexpectedly, we encountered an unusual issue. Please look at the image below. [img]{STEAM_CLAN_IMAGE}/44884140/9ed73862cb9b08fdaa3a1b1ebc4d5dafa8672876.gif[/img] The character’s brow width narrows and widens! [img]{STEAM_CLAN_IMAGE}/44884140/3e5b9546e832803ee5e9fc75dce0d814cbf981a6.gif[/img] [img]{STEAM_CLAN_IMAGE}/44884140/271a944fb820b224811d2daa17b869342a3756f5.png[/img] We discovered that this mysterious issue occurs along the diagonal lines splitting the sprite mesh, but unfortunately, we haven’t found a fundamental solution yet. Coincidentally, we saw similar issues in pixel art games using other engines, suggesting that it may be an unavoidable problem specific to pixel art games that don’t use anti-aliasing, independent of the engine. We believe this phenomenon is related to floating-point calculation errors. We tried slightly adjusting the camera position relative to the character to reduce these errors, which helped to prevent the main character’s brow from widening, though it wasn’t a complete fix. (If anyone knows, please let us know!) We ended this development log with a lingering issue, but this is the conclusion for now. Thank you for reading!