Overview
My objective for this project was to study how HLSL shaders and post processing effects worked in Unity's universal render pipeline. To provide myself with a direction, I aimed to deconstruct/reconstruct the 2D pixel art aesthetic in Unity's 3D engine.
Pixel Perfect Outlines
In pixel art, single-pixel outlines are used to define the edges of shapes. The blocky appearance of the lines is responsible for giving pixel art its iconic "chunky" look. To best replicate these lines, I developed a screenspace shader (initially written in HLSL, later ported to URP's Shader Graph for developmental flexibility) to mark the edge pixels of 3D objects in the unity scene. The screen's resolution is downscaled prior to drawing the outlines, completing the pixelated effect.
I used sobel operations to detect extreme changes in the camera's depth and normal buffers, providing the shader with a map of where edges are located. Depth is used to mark the outer edges of objects, while normals are used to mark the inner edges.
Water Shader and Custom Reflection Probe
To produce the illusion of realtime water reflections, a second camera below the surface of the water uses trigonometry to perfectly mirror the rotation and position of the main camera. To prevent the reflection probe from rendering geometry above the surface of the water, I culled the secondary camera's frustums by the water plane.
Pixel Perfect Camera
To prevent the moving camera from sampling the world at uneven points and producing a jittering discontinuity, I locked its position to a grid of pixel-sized units (based on the camera's resolution). The grid is multiplied by the camera's yaw and pitch to ensure it correctly moves the camera along pixel-sized increments from any angle.
To negate the blocky repositioning produced by the grid's pixel-sized units, the positional offset imparted by the grid is subtracted from the final position of the camera using a plane-based camera rig. The end result preserves the grid's pixel perfect sampling without compromising the smooth glide of the camera.
AOV Creation Tool
The screenspace nature of the shader prevents it from accessing scene data by default, necessitating the development of a custom buffer system to write additional AOVs (arbitrary output variables) for the shader to read and use. Custom AOVs play an integral role in layer masking and reading object information.
To create custom AOVs, I developed the "Generate Buffer" rendering pass. The pass renders objects from a specified layer to an RTHandle. The handle is sent to the shader as a screenspace texture, where the per-pixel data it contains can be accessed by sampling texture coordinates. An override shader can be applied to objects rendered to the RTHandle, allowing for the insertion of AOV data.