<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tommy</title>
    <description>The latest articles on DEV Community by tommy (@tommy91dev).</description>
    <link>https://dev.to/tommy91dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190311%2F15adb668-22a9-4c46-bf52-c532258dbecc.png</url>
      <title>DEV Community: tommy</title>
      <link>https://dev.to/tommy91dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tommy91dev"/>
    <language>en</language>
    <item>
      <title>How to get the PS1 look in Godot 4 (vertex snapping, affine warping, dither)</title>
      <dc:creator>tommy</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:31:32 +0000</pubDate>
      <link>https://dev.to/tommy91dev/how-to-get-the-ps1-look-in-godot-4-vertex-snapping-affine-warping-dither-57mo</link>
      <guid>https://dev.to/tommy91dev/how-to-get-the-ps1-look-in-godot-4-vertex-snapping-affine-warping-dither-57mo</guid>
      <description>&lt;p&gt;The PS1 look isn't one effect. It's four or five hardware limits stacked on top of each other. Fake each one in a shader and the style falls out by itself. Here's how each piece works in Godot 4, with the actual shader code I use, plus one mistake that made my floors look broken.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffeyhje3xva6aem0riejv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffeyhje3xva6aem0riejv.gif" alt="Stock Godot vs PS1 wobble" width="320" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Vertex snapping: the "wobble"
&lt;/h2&gt;

&lt;p&gt;The PS1 had no sub-pixel precision, so vertices snapped to whole screen pixels. As the camera moves, geometry jitters. You can fake this by snapping the clip-space position to a coarse grid in the vertex shader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="nf"&gt;ps1_snap_clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ps1_snap_grid&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// e.g. (320, 240) * 0.5&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;ndc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// to normalized device coords&lt;/span&gt;
    &lt;span class="n"&gt;ndc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ndc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// round to the grid&lt;/span&gt;
    &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ndc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// back to clip space&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;vertex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PROJECTION_MATRIX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODELVIEW_MATRIX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VERTEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;POSITION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ps1_snap_clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Divide by &lt;code&gt;w&lt;/code&gt; first, snap, then multiply back. If you snap raw clip coords, distant objects barely wobble and near ones wobble too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Affine texture warping
&lt;/h2&gt;

&lt;p&gt;The PS1 interpolated UVs linearly in screen space, with no perspective correction. That's why textures swim and bend. GPUs correct perspective for you, so you undo it: pass &lt;code&gt;UV * w&lt;/code&gt; and &lt;code&gt;w&lt;/code&gt; as a varying, then divide in the fragment shader.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="k"&gt;varying&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;v_affine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;vertex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...after POSITION is set:&lt;/span&gt;
    &lt;span class="n"&gt;v_affine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UV&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;POSITION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;POSITION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;affine_uv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_affine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;v_affine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// screen-linear = PS1-style&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;affine_uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ps1_affine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0 = modern, 1 = full PS1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The floor-smear lesson
&lt;/h3&gt;

&lt;p&gt;My first version used full affine everywhere. Walls looked great, but the ground looked like smeared AI slop: pixels streaking into each other. The reason is that affine error grows with how much depth a triangle covers on screen. A floor made of two huge triangles running from your feet to the horizon is the worst case.&lt;/p&gt;

&lt;p&gt;Three fixes, in order of impact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subdivide big planes.&lt;/strong&gt; Smaller triangles mean less depth per triangle and less warp. (The real PS1 games did exactly this.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower affine per material.&lt;/strong&gt; I default floors to about &lt;code&gt;0.12&lt;/code&gt; and leave walls at &lt;code&gt;0.5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fade it out where it hurts most&lt;/strong&gt;: close to the camera, and on triangles whose depth changes fast across the screen:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="nf"&gt;ps1_affine_uv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;mult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ps1_affine&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;smoothstep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// fade near the camera&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fwidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mo"&gt;0001&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// steep depth across the screen?&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;smoothstep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mo"&gt;02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mo"&gt;0001&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You keep the swimming on walls and props, and floors stay readable. Use &lt;code&gt;filter_nearest_mipmap&lt;/code&gt; on the texture too, so distant floors don't shimmer into lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Low resolution, done properly
&lt;/h2&gt;

&lt;p&gt;Don't fake low-res with a pixelate shader. Render the 3D world into a &lt;code&gt;SubViewport&lt;/code&gt; at 320x240 and draw it upscaled with nearest filtering. Then snapping, dithering and texture pixels all line up on the same grid. That alignment is a big part of why it reads as authentic and not as a filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Colour depth + Bayer dither
&lt;/h2&gt;

&lt;p&gt;The PS1 output 15-bit colour (5 bits per channel) and dithered to hide the banding. In a fullscreen &lt;code&gt;canvas_item&lt;/code&gt; shader on the low-res image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;B4&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;[](&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.);&lt;/span&gt;

&lt;span class="kt"&gt;ivec2&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ivec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UV&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;low_res&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                   &lt;span class="c1"&gt;// low-res pixel coords&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B4&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// threshold 0..1&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exp2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color_bits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// 5 bits -&amp;gt; 31 levels&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dither_strength&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; full-strength dither puts ugly grain on flat bright skies. I measure local contrast from four neighbouring pixels and cut the dither on flat, bright areas, keeping it in darks and gradients where it actually hides banding.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The finishing touches
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gouraud lighting:&lt;/strong&gt; &lt;code&gt;render_mode vertex_lighting;&lt;/code&gt; in a spatial shader. Lighting per vertex, not per pixel, very PS1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-vertex fog:&lt;/strong&gt; compute fog in &lt;code&gt;vertex()&lt;/code&gt; from view distance and set the background to the same colour. That's how PS1 games hid their short draw distance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global shader parameters:&lt;/strong&gt; put the snap grid, affine amount, fog and so on in Project Settings globals. Then one setting changes every material, and an "off" switch for before/after comparisons is trivial.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Snap vertices, undo perspective correction (carefully), render low-res, quantize and dither. That's 90% of the look. Start with the snippets above, and remember to subdivide your floors.&lt;/p&gt;




&lt;p&gt;If you'd rather skip the plumbing, I packaged all of this as the &lt;strong&gt;&lt;a href="https://ahmed-g9.itch.io/godot4-ps1-shader-kit" rel="noopener noreferrer"&gt;Godot 4 PS1 Shader Kit&lt;/a&gt;&lt;/strong&gt; ($1.99, MIT). It has the lit and unshaded shaders, the 320x240 pixel camera, the dither post, N64 3-point filtering, PS1/N64/Saturn presets and two demo scenes. My other Godot packs are in the &lt;strong&gt;&lt;a href="https://ahmed-g9.itch.io/godot-4-ai-dev-bundle" rel="noopener noreferrer"&gt;Godot 4 Mega Bundle&lt;/a&gt;&lt;/strong&gt; ($14.99).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I sell the kit linked above (no affiliate links). The code was written with an AI coding assistant and tested by hand in Godot 4.5.1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>shaders</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>2 AI agent skills for indie game devs: Godot 4 code that actually runs, and a Steam page audit</title>
      <dc:creator>tommy</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:52:30 +0000</pubDate>
      <link>https://dev.to/tommy91dev/2-ai-agent-skills-for-indie-game-devs-godot-4-code-that-actually-runs-and-a-steam-page-audit-12g1</link>
      <guid>https://dev.to/tommy91dev/2-ai-agent-skills-for-indie-game-devs-godot-4-code-that-actually-runs-and-a-steam-page-audit-12g1</guid>
      <description>&lt;p&gt;If you use Cursor or Claude Code for game dev, you've seen it: the agent writes Godot 3 code that breaks in Godot 4. And your Steam page is the thing that decides your wishlists. I built a skill for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Godot 4 Gameplay Architect (€9)
&lt;/h2&gt;

&lt;p&gt;Stops the agent from writing outdated GDScript. It has a table of 40+ Godot 3 to 4 renames and removals (yield vs await, KinematicBody2D vs CharacterBody2D, export syntax, signals and more), ready patterns for state machines, components, save systems and object pooling, and a pre-release checklist.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/godot4-gameplay-architect" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/godot4-gameplay-architect&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Steam Page Doctor (€5)
&lt;/h2&gt;

&lt;p&gt;Give your agent your Steam store page and it returns a scored audit (100-point weighted rubric), prioritised fixes, ready-to-paste short description and About This Game copy, a 20-tag plan, and a timeline for Coming Soon, Next Fest and launch. It never invents claims about your game.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/steam-page-doctor" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/steam-page-doctor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both install as SKILL.md skills in Claude Code, Cursor and Codex CLI. What's your agent's worst Godot habit?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I made these skills and they're paid.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>ai</category>
      <category>steam</category>
    </item>
    <item>
      <title>4 agent skills I built for the bugs AI coding agents keep shipping</title>
      <dc:creator>tommy</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:51:43 +0000</pubDate>
      <link>https://dev.to/tommy91dev/4-agent-skills-i-built-for-the-bugs-ai-coding-agents-keep-shipping-2g5m</link>
      <guid>https://dev.to/tommy91dev/4-agent-skills-i-built-for-the-bugs-ai-coding-agents-keep-shipping-2g5m</guid>
      <description>&lt;p&gt;Cursor, Claude Code and Codex are fast, but they keep making the same expensive mistakes. I packaged fixes for four of them as installable SKILL.md agent skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stripe Subscription Guard (€7)
&lt;/h2&gt;

&lt;p&gt;The silent Stripe bug: the webhook returns 200 but the database never updates, so cancelled users keep access, or an upgrade creates a second subscription and charges twice. This skill gives the agent 10 rules plus a map of exactly which DB change each Stripe event must make. Works with any stack.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/stripe-subscription-guard" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/stripe-subscription-guard&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SaaS AuthZ Audit (€31)
&lt;/h2&gt;

&lt;p&gt;The most common web flaw: user A can read user B's data. 14 concrete patterns with how to detect and fix each, Supabase and Firebase database rules, notes for 6 frameworks, a two-tenant test plan with ready-made regression tests, and a script that inventories every endpoint.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/saas-authz-audit" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/saas-authz-audit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. MCP Tool Hardening (€27)
&lt;/h2&gt;

&lt;p&gt;For building MCP servers, or deciding whether to install someone else's. 12 rules, checks for hidden instructions and silent tool updates, remote-server auth guidance, 60+ attack test inputs, a 50-point scored audit and a standalone scanner.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/mcp-tool-hardening" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/mcp-tool-hardening&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Repo Docs Autopilot (€35)
&lt;/h2&gt;

&lt;p&gt;Docs that don't lie: every claim must point to a file, command output or commit, and commands are actually run before they go in the README. Covers README, API reference, architecture diagrams, changelog from git history, AGENTS.md/CLAUDE.md, plus a check that fails CI when docs go stale.&lt;br&gt;
&lt;a href="https://skillhq.dev/skills/tommy/repo-docs-autopilot" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/repo-docs-autopilot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which of these mistakes has your agent made? Happy to answer questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I made these skills and they're paid.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>stripe</category>
      <category>security</category>
    </item>
    <item>
      <title>I turned my Next.js + Supabase safety rules into installable agent skills (free + €2)</title>
      <dc:creator>tommy</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:55:56 +0000</pubDate>
      <link>https://dev.to/tommy91dev/i-turned-my-nextjs-supabase-safety-rules-into-installable-agent-skills-free-eu2-l</link>
      <guid>https://dev.to/tommy91dev/i-turned-my-nextjs-supabase-safety-rules-into-installable-agent-skills-free-eu2-l</guid>
      <description>&lt;p&gt;After my last post on the 7 mistakes AI coding agents make in Next.js + Supabase apps, a few people asked for the rules in a drop-in format. So I packaged them as SKILL.md agent skills that work in Claude Code, Cursor and Codex CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free: ShipSafe Lite
&lt;/h2&gt;

&lt;p&gt;Four rules that stop the worst mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the Supabase service role key server-side only&lt;/li&gt;
&lt;li&gt;RLS on for every table, with real policies&lt;/li&gt;
&lt;li&gt;Verify auth on the server with &lt;code&gt;getUser()&lt;/code&gt;, never trust the client&lt;/li&gt;
&lt;li&gt;Stay in scope: no surprise refactors or migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get it: &lt;a href="https://skillhq.dev/skills/tommy/shipsafe-nextjs-supabase-lite" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/shipsafe-nextjs-supabase-lite&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Full: ShipSafe Rules (€2)
&lt;/h2&gt;

&lt;p&gt;All 12 rules (secrets, RLS, auth, rate limits, errors and logging, Stripe webhooks, uploads and storage, AI endpoints, background jobs, PII in logs, a ship checklist), plus a pre-deploy audit prompt, Claude Code settings that block reading &lt;code&gt;.env&lt;/code&gt; and destructive Supabase commands, and an AGENTS.md for Codex.&lt;/p&gt;

&lt;p&gt;Get it: &lt;a href="https://skillhq.dev/skills/tommy/shipsafe-nextjs-supabase" rel="noopener noreferrer"&gt;https://skillhq.dev/skills/tommy/shipsafe-nextjs-supabase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome: which mistake does your agent make most?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I made these and the full version is paid.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>nextjs</category>
      <category>supabase</category>
    </item>
    <item>
      <title>7 mistakes AI coding agents make in Next.js + Supabase apps (and the rules that stop them)</title>
      <dc:creator>tommy</dc:creator>
      <pubDate>Sat, 17 Oct 2026 10:41:13 +0000</pubDate>
      <link>https://dev.to/tommy91dev/7-mistakes-ai-coding-agents-make-in-nextjs-supabase-apps-and-the-rules-that-stop-them-1fl3</link>
      <guid>https://dev.to/tommy91dev/7-mistakes-ai-coding-agents-make-in-nextjs-supabase-apps-and-the-rules-that-stop-them-1fl3</guid>
      <description>&lt;p&gt;AI agents like Cursor and Claude Code write working code fast. They also ship the same handful of dangerous mistakes over and over, because "it runs" is not the same as "it's safe." Here are the seven I see most in Next.js + Supabase projects, plus the one-line rule you can drop into your agent's instructions to prevent each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Using the service role key in client code
&lt;/h2&gt;

&lt;p&gt;The agent hits a permissions error, grabs the &lt;code&gt;service_role&lt;/code&gt; key, and the error goes away. So does your Row Level Security.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Never import or reference the Supabase service role key outside server-only files. If a query fails on RLS, fix the policy, not the key."&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Tables with RLS turned off
&lt;/h2&gt;

&lt;p&gt;New tables get created, queries work in dev, and nobody enables RLS.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Every new table must enable RLS and ship with explicit select/insert/update/delete policies in the same migration."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Trusting the client for auth checks
&lt;/h2&gt;

&lt;p&gt;Hiding a button is not authorization.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Check the session and ownership on the server in every Route Handler and Server Action that reads or writes user data."&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Webhooks without signature checks or idempotency
&lt;/h2&gt;

&lt;p&gt;Stripe-style webhooks get processed unverified, and retries grant access twice.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Verify the webhook signature before parsing, and make handlers idempotent using the event ID."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Secrets leaking via NEXT_PUBLIC_
&lt;/h2&gt;

&lt;p&gt;Anything prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; is shipped to the browser.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Only public, non-secret values may use the NEXT_PUBLIC_ prefix."&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Migrations that lock big tables
&lt;/h2&gt;

&lt;p&gt;Adding an index or a NOT NULL column on a large table can lock it in production.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Create indexes CONCURRENTLY, add columns nullable first, backfill, then add constraints. Include a rollback step."&lt;/p&gt;

&lt;h2&gt;
  
  
  7. N+1 queries hidden in loops
&lt;/h2&gt;

&lt;p&gt;The agent fetches related rows one at a time inside a &lt;code&gt;.map()&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; "Never query inside a loop. Batch with &lt;code&gt;in()&lt;/code&gt; filters or joins, and paginate every list query."&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually make the agent follow these
&lt;/h2&gt;

&lt;p&gt;Put them where the agent reads them every session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code: &lt;code&gt;CLAUDE.md&lt;/code&gt; in the repo root&lt;/li&gt;
&lt;li&gt;Cursor: &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Codex and others: &lt;code&gt;AGENTS.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep each rule short and specific. Vague rules like "write secure code" get ignored; concrete ones like "never use the service role key in client code" stick.&lt;/p&gt;

&lt;p&gt;For reviews, a second pass by a narrowly scoped "reviewer" agent (security, payments, migrations) catches far more than asking the same agent that wrote the code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I sell two packs built around this approach. &lt;a href="https://tommy91.gumroad.com/l/meihez" rel="noopener noreferrer"&gt;ShipSafe Rules&lt;/a&gt; is ready-made CLAUDE.md, AGENTS.md and Cursor rules for Next.js + Supabase. The &lt;a href="https://tommy91.gumroad.com/l/senior-agents" rel="noopener noreferrer"&gt;Senior Engineer Agent Pack&lt;/a&gt; is five reviewer subagents for security, payments, migrations, performance and tests. Everything above works without them. Copy the rules and use them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>cursor</category>
      <category>security</category>
    </item>
  </channel>
</rss>
