psx-retro-rendering is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. PlayStation 1 rendering in Summer Engine — low-res output, RGB5 and exact dither, affine textures, vertex snapping, blend modes, fog; limits named.
When your agent uses it
- Make my game render like a PlayStation 1 game, with texture warping, vertex wobble, and dithering
- Add a PSX retro shader to the 3D scene
- Check an existing PS1-style shader against what the real hardware did
- My retro shader looks merely pixelated, not like a real PS1 game
Install
summer setup installs every skill for your agent. To add only this one:
claude-code for your agent, such as codex or cursor. See Skills for every option.
The skill
Outcome
Recreate the visible behavior of the original PlayStation rendering pipeline while keeping three things distinct:- behavior the PS1 actually had;
- the engine technique used to imitate it;
- places where the imitation cannot reproduce the hardware exactly.
https://github.com/wyvernbw/godot-psxlike- Treat it as a reference implementation, not as proof that every approximation is hardware-exact, compatible with the current Summer Engine build, or suitable for every scene.
When to Use
- The game targets a PS1/PSX look and needs more than low resolution and vertex wobble.
- An existing PSX shader needs to be checked against hardware behavior.
- Artists need predictable rules for palettes, mesh subdivision, vertex colors, transparency, or fog.
Authenticity Boundary
The PS1 GPU is fundamentally a 2D ordered polygon rasterizer. Avoid saying that it had a “flat depth buffer” or that its Z-fighting came from flat per-polygon depth; it had no depth buffer at all.
Summer Engine Integration
ShaderMaterial supports shared shaders, material uniforms, and per-instance uniforms through GeometryInstance3D.set_instance_shader_parameter(). Prefer per-instance uniforms over duplicating materials when changing mesh-local PSX settings.
In Summer’s material workflow, attach a .tres material override to the MeshInstance3D, not its parent Node3D, and save the scene after assignment. A custom PSX pipeline is an appropriate ShaderMaterial use because StandardMaterial3D cannot express affine interpolation, screen-coordinate snapping, or fixed framebuffer blend equations.
Use this project shape:
WorldEnvironment or camera environment cannot replace mesh materials.
Shared materials, texture arrays, instance uniforms, and Forward+ can improve batching opportunities. They do not guarantee that arbitrary meshes render in one draw call. Transparent and opaque rendering already require separate materials.
Check compatibility-sensitive names and shader behavior against the installed Summer Engine build.
Agent Procedure
Implement one visible layer at a time. After each layer, run the game and compare a controlled test scene before composing the next layer. Do not diagnose a combined shader when a low-resolution, color-space, affine, depth, transparency, lighting, or fog boundary can be tested independently.1. Establish the Low-Resolution Output
Use project settings where possible. At runtime, configure the rootWindow, not a nonexistent content_scale_integer property:
2. Keep the Color Pipeline Explicit
RGB5 quantization and the PS1 dither operate on display-encoded RGB values, not on linear-light values. The Forward+ color conversions can otherwise change the palette and dither response. Choose one coherent texture path:- Raw sRGB samples: omit
source_coloron the texture uniform, perform modulation/dither/RGB5 in sRGB, then convert to linear before assigningALBEDO. - Linear samples: use
source_color, convert the sample back to sRGB before PS1 operations, then convert the final result back to linear.
source_color arrive in linear space and must be converted before combining them with raw sRGB texels.
The end of an opaque fragment pipeline should have this order:
3. Quantize to RGB5 and Use the Exact Dither
Useround() rather than floor() in the final implementation. With fog and dithering, floor() can make a flat RGB5-representable color alternate with the next darker shade.
4. Sample and Author Textures Like PS1 Assets
The PS1 does not bilinearly filter textures. Sample exact texels at mip level zero:[0, 1].
Full CLUT/VRAM emulation provides no modern performance benefit and complicates authoring. To reproduce the visible palette limitation, author indexed textures with 16 colors for a 4-bit-style asset or 256 colors for an 8-bit-style asset.
On PS1 textured polygons, a pure-black texel is transparent. If this behavior is enabled, discard black only for the relevant textured path; do not accidentally discard an intentionally black untextured polygon:
5. Support Raw Textures, Vertex Colors, and Texture Modulation
PS1 polygons can be flat- or Gouraud-shaded, textured or untextured, and raw-textured or texture-modulated. Preserve those as explicit modes. True per-triangle flat colors are awkward in an ordinary mesh shader. Practical choices are:- use one
flat_colorinstance uniform for a whole mesh; - duplicate triangle vertices and bake the same color into all three;
- use Gouraud mode with equal colors at a triangle’s vertices.
vec3(0.5), is neutral. Expose raw texture mode separately so textures can bypass vertex-color modulation.
6. Cancel Perspective-Correct UV Interpolation
Merely dividing ordinary interpolated UVs byw is incomplete. Multiply at each vertex first, allow the GPU to interpolate, then divide in the fragment shader:
7. Quantize Vertices in Screen Space
The wobble comes from integer screen coordinates and the absence of subpixel rasterization. Quantize after projection, relative to the actual low-resolution viewport:POSITION and for the clip position carried to the fragment shader. Do not snap local-space vertices to an arbitrary world grid; that produces a different motion artifact. Keep the snap resolution synchronized with the render viewport and visually verify the exact pixel cadence.
8. Treat Flat Depth as an Approximation
The original GPU drew polygons in submission order. A spatial shader cannot reproduce the CPU ordering table, so the reference shader uses a non-interpolated depth value:- A
flatvarying normally comes from one provoking vertex, not the average of the triangle. - A large floor can cover nearer objects because one value represents the entire triangle.
- Intersecting meshes can pop as whole triangles rather than resolve per pixel.
depth_modifieris an artist-controlled ordering bias and is most manageable with fixed cameras.- Subdivide large surfaces first. Use the bias only for residual ordering problems.
9. Calculate Lighting Before Fragment Quantization
On original hardware, the CPU/GTE calculates vertex colors. Normals are not interpolated and lit per fragment. The GTE provides a basic three-light directional model; games can also implement their own point-light logic. The normallight() path is per fragment. Enabling vertex lighting gets closer geometrically, but lighting can be applied after the custom fragment pipeline, which leaves gradients too smooth and bypasses the intended RGB5/dither response.
For a consistent result:
- collect the supported lights in GDScript;
- pack type, visibility, range, direction or position, and color into an
Image.FORMAT_RGBFdata texture; - sample that texture with
texelFetch()invertex(); - calculate Lambert-style illumination per vertex;
- multiply or otherwise combine it with
COLORbefore affine interpolation, fog, dithering, and RGB5 conversion.
10. Implement All Four Semi-Transparency Modes
For existing framebuffer colorB and incoming fragment color F, support:
These are fixed blend modes, not ordinary source-alpha blending. Screen-texture access moves the material into the transparent pass. Keep common pipeline code in a
.gdshaderinc file and provide separate opaque and transparent shaders.
Apply the selected blend before the final dither, RGB5 quantization, and linear conversion:
11. Choose the Intended Fog Model
First compute a clamped near-to-far factor and quantize it to the GTE’s 12-bit-style depth-cue range:view_distance / (far - near) expression omits the near-plane subtraction. Preserve it only when matching that addon’s existing visuals; use the normalized expression above when near_distance is intended to mark the start of fog.
Support two modes and label them accurately:
- Modulation fog: blend the vertex color toward the fog color, then texture-modulate. This is hardware-plausible but only resembles a conventional fog
lerpwhen the fog color is black. Colored modulation fog can become overbright or otherwise look “wrong”; that is authentic to the operation. - Silent Hill-style fog: the authentic technique draws world triangles twice—an untextured fog-color contribution and a textured contribution—using additive semi-transparency. This roughly halves available geometry throughput. A single fragment-shader
mix()is a cheaper visual approximation, not the original technique.
round() in to_rgb5(), prevents a visually flat fog region from acquiring unnecessary alternating dither shades.
Pipeline Invariants
Preserve these relationships even if the implementation is reorganized:- Vertex snapping happens after projection and before rasterization.
- Affine UV compensation multiplies at the vertex and divides after interpolation.
- Vertex lighting and modulation happen before fragment dithering and quantization.
- Dithering uses fragment coordinates and precedes RGB5 quantization.
- RGB5 and dithering operate in sRGB; the final
ALBEDOis converted to linear. - Flat depth is explicitly labeled as an approximation of ordered drawing.
- Opaque and transparent materials share code but remain separate render paths.
- Silent Hill-style single-pass fog is labeled as an approximation if geometry is not actually rendered twice.
Tunables
Failure Modes
- The result is merely pixelated: affine UVs, screen-coordinate snapping, RGB5, or conditional dithering is missing.
- Dither appears on every flat surface: it was applied fullscreen or without the Gouraud/modulation eligibility check.
- Colors differ between Compatibility and Forward+: linear and sRGB operations are mixed or performed twice.
- Textures remain stable at oblique angles: UVs were not multiplied by vertex
wbefore interpolation. - Textures explode on large surfaces: subdivide geometry or reduce grazing camera angles; affine mapping is working but uncontrolled.
- A floor covers nearby meshes: this is a flat-depth proxy failure; subdivide first, then apply a small depth modifier.
- Transparent objects do not blend with each other: this is the screen-texture/transparent-pass limitation.
- Lighting gradients look modern and smooth: the engine’s per-fragment lighting is bypassing the custom vertex-color pipeline.
- Colored modulation fog looks too bright: modulation is not a conventional color lerp; choose black or use the labeled Silent Hill-style approximation.
- Flat fog is excessively patterned: use an RGB5-representable fog color, dither before quantizing, and quantize with
round(). - Batching is worse than expected: different meshes, materials, transparency paths, shader variants, or instance state can split batches; shared materials do not guarantee one draw call.
Verification
Verify the running client, not only shader compilation:- Confirm the root 3D scene renders at the intended base resolution and scales cleanly at several window sizes.
- Render an sRGB gradient and compare RGB5 bands with dithering on and off.
- Confirm a flat unmodulated polygon does not dither while a Gouraud or modulated polygon does.
- Move the camera slowly and verify vertices step in screen pixels rather than in world-space increments.
- Rotate a large textured quad to expose affine warping, then subdivide it and confirm the distortion decreases.
- Overlap intersecting triangles and verify the chosen flat-depth approximation and depth modifiers are understood.
- Test raw texture mode, half-gray neutral modulation, and the pure-black transparency rule independently.
- Compare a custom vertex-lit mesh against the engine’s per-pixel lighting and confirm the light gradient passes through dither and RGB5.
- Exercise every transparency equation over known background colors, then separately demonstrate the overlapping-transparent limitation.
- Test black modulation fog, colored modulation fog, and the Silent Hill-style approximation at the near and far planes.
- Capture the same scene in Compatibility and Forward+ if both are supported; investigate color-space or feature differences rather than assuming parity.
Confidence
extracted — Hardware mechanisms and the engine techniques are drawn from Calin P’s hardware-informed article and MIT reference addon. The skill explicitly identifies modern approximations and known limitations. It has not been verified end to end inside Summer Engine. Test the visible client flow and renderer-specific behavior before calling a project verified.
Related skills
art-direction: Define the game’s visual style (palette, mood, lighting, post) as an art bible in .summer/art-bible.md.3d-lighting: Light a 3D scene: sun, omni and spot lights, WorldEnvironment, sky, shadows and ambient.
Source:
library/skills/psx-retro-rendering/SKILL.md, rendered from summer-engine@4.3.0 on npm.
