{"author_link":"\/users\/saering","author_name":"Saering","author_uid":"saering","comments":[],"epoch":1776881801,"event":"LD59","format":"md","ldjam_node_id":431663,"likes":23,"metadata":{"p_key":"96507","p_author":"Saering","p_authorkey":"1081341","p_urlkey":"312149","p_title":"Rendering breakdown","p_cat":"LDJam ","p_event":"LD59","p_time":"1776881801","p_likes":"23","p_comments":"0","p_status":"WAYBACK","us_key":"1081341","us_name":"Saering","us_username":"saering","event_start":"1776470400","event_key":"113","event_name":"Ludum Dare 59"},"node":{"_collation":{"body_sanitizer":"TextUtils::SanitizeHTML via existing importer","event":"LD59","removed_author":false},"_superparent":424249,"_trust":6,"author":81341,"body":"Hi there! :satellite: \n\nPeople seem to enjoy [**Kuiper Space Rescue**](https:\/\/ldjam.com\/events\/ludum-dare\/59\/kuiper-space-rescue) aesthetics and atmosphere a lot, so I thought I'd share how the rendering was made! Nothing very complex, but hopefully this will give some people ideas or inspiration!\nThe breakdown is about how to go from the left screenshot to the right one:\n\n![before after.png](\/\/\/raw\/dbd\/31\/z\/72542.png)\n\nThe effect looks like this in game:\n\n![kuiper-ezgif.com-optimize (1).gif](\/\/\/raw\/dbd\/31\/z\/72543.gif)\n\n---\n\nReference image:\n\n![ref.gif](\/\/\/raw\/dbd\/31\/z\/72544.gif)\n\nThe idea was not to copy it perfectly, simply to have a starting point to write the shader.\n\nThe game has been made with **Godot**, so the post process setup is a simple CanvasLayer node containing a ColorRect rendered above the game. On this ColorRect is attached a material with the post process shader this breakdown is about.\n\nThe very first thing I've done is adding a bit of **chromatic aberration**, making the image a bit less clean, and most importantly splitting the image into a **yellow** and a **red** layer, the yellow being rendered on top for the global tint.\n\n![aberration.gif](\/\/\/raw\/dbd\/31\/z\/72545.gif)\n\n![ksr_aberration.png](\/\/\/raw\/dbd\/31\/z\/72546.png)\n\nNext effect is adding more red. On the reference, the red color is more diffuse than the yellow, but always near it. A **blur function** is a perfect fit for this. Cool thing is, it does not really matter what blur function is used, since the effect tries to mimic an old machine screen, even a low quality blur can work perfectly fine.\n\n![ksr_redblur.png](\/\/\/raw\/dbd\/31\/z\/72548.png)\n\nThen, the **blueish** color comes in. On the reference it looks like it's leaking horizontally, which again is a nice fit for a blur, this time a **directional blur**. I went for a blur going in 5 directions instead of a horizontal one as we thought it looked better.\nHere's a gif showing the blur parameters in action:\n\n![blueblur-ezgif.com-optimize.gif](\/\/\/raw\/dbd\/31\/z\/7254a.gif)\n\n![ksr_blueblur.png](\/\/\/raw\/dbd\/31\/z\/7254b.png)\n\nThen, a mandatory **grain** effect is added on top of everything.\n\n![ksr_grain.png](\/\/\/raw\/dbd\/31\/z\/7254d.png)\n\nAdding the grain to the rest looks like this:\n\n![ksr_sum.png](\/\/\/raw\/dbd\/31\/z\/7254e.png)\n\nThe rest of the shader is a very light color correction and another grain pass added on top to make the yellow part of the game a bit more dirty.\n\n![ksr_final.png](\/\/\/raw\/dbd\/31\/z\/7254f.png)\n\n---\n\nAnd that's it!\nWhat's great with the effect was that, any asset drawn in pure white looked good once added to the scene. The logo of the game was even exported directly from there:\n\n![image (2).png](\/\/\/raw\/dbd\/31\/z\/72557.png)\n\nThanks for reading! :v: \n\nLast thing to do is to share the shader code! Do not consider it a perfectly written shader, it's still a game jam implementation, but if there's anything you want to take from it, there you go!\n\n```\nshader_type canvas_item;\n\nuniform sampler2D screen_texture: hint_screen_texture;\n\ngroup_uniforms Aberration;\nuniform float aberration_offset: hint_range(0.0, 1.0) = 0.01;\nuniform float aberration_yellow_reduction: hint_range(0.0, 1.0) = 0.1;\n\ngroup_uniforms RedBlur;\nuniform float red_blurriness: hint_range(0, 128, 1) = 64;\nuniform int red_iterations: hint_range(0, 128, 1) = 64;\nuniform int red_quality: hint_range(0, 128, 1) = 32;\nuniform float red_blur_intensity: hint_range(0.0, 10.0) = 3.0;\nuniform vec3 red_blur_color: source_color = vec3(1.0, 0.0, 0.0);\n\ngroup_uniforms BlueBlur;\nuniform float blue_blur_blurriness: hint_range(0, 128, 1) = 8;\nuniform int blue_blur_iterations: hint_range(0, 128, 1) = 8;\nuniform int blue_blur_quality: hint_range(0, 128, 1) = 8;\nuniform float blue_blur_intensity: hint_range(0.0, 10.0) = 1.0;\nuniform vec3 blue_blur_color: source_color = vec3(0.0, 0.0, 1.0);\n\ngroup_uniforms Grain;\nuniform sampler2D grain_texture: repeat_enable;\nuniform float grain_speed: hint_range(0.0, 10.0) = 0.1;\nuniform float grain_intensity: hint_range(0.0, 1.0) = 0.1;\n\nvec3 hash23(vec2 input)\n{\n    float a = dot(input.xyx, vec3(127.1, 311.7, 74.7));\n    float b = dot(input.yxx, vec3(269.5, 183.3, 246.1));\n    float c = dot(input.xyy, vec3(113.5, 271.9, 124.6));\n    return fract(sin(vec3(a, b, c)) * 43758.5453123);\n}\n\nvec4 texture_xorgaussian(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, int iterations, int quality)\n{\n\tvec2 radius = blurriness \/ (1.0 \/ pixel_size).xy;\n\tvec4 blurred_tex = texture(tex, uv);\n\t\n\tfor (float d = 0.0; d < TAU; d += TAU \/ float(iterations))\n\t{\n\t\tfor (float i = 1.0 \/ float(quality); i <= 1.0; i += 1.0 \/ float(quality))\n\t\t{\n\t\t\tvec2 directions = uv + vec2(cos(d), sin(d)) * radius * i;\n\t\t\tblurred_tex += texture(tex, directions);\n\t\t}\n\t}\n\t\n\tblurred_tex \/= float(quality) * float(iterations) + 1.0;\n\treturn blurred_tex;\n}\n\nfloat luminance(vec3 color)\n{\n    const vec3 w = vec3(0.2125, 0.7154, 0.0721);\n    return dot(color, w);\n}\n\nvoid fragment()\n{\n\tvec3 screen_color = texture(screen_texture, SCREEN_UV).rgb;\n\n\t\/\/ Red blur.\n    vec3 red_blur = texture_xorgaussian(screen_texture, SCREEN_UV, SCREEN_PIXEL_SIZE, red_blurriness, red_iterations, red_quality).rgb;\n\tred_blur = clamp(red_blur, 0.0, 1.0);\n\t\n\t\/\/ Blue blur.\n\tvec3 blue_blur = texture_xorgaussian(screen_texture, SCREEN_UV, SCREEN_PIXEL_SIZE, blue_blur_blurriness, blue_blur_iterations, blue_blur_quality).rgb;\n\tblue_blur = clamp(blue_blur, 0.0, 1.0);\n\t\n\t\/\/ Aberration.\n\tvec2 uv_offset = vec2(aberration_offset, 0.0);\n\tvec4 aberration_right = texture(screen_texture, SCREEN_UV + uv_offset);\n\tvec4 aberration_left = texture(screen_texture, SCREEN_UV - uv_offset);\n\tvec3 aberration = vec3((aberration_left.x + aberration_right.x) * 0.8, aberration_right.y * 1.0, 0.0);\n\taberration = clamp(aberration, 0.0, 1.0);\n\t\n\t\/\/ Grain.\n\tfloat grain_time = fract(TIME * grain_speed);\n\tfloat grain_sample = 0.0;\n\tif (grain_time < 0.333f)\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0) + vec2(0.2, 0.2)).r;\n\telse if (grain_time < 0.666f)\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0) + vec2(0.3, 0.1)).r;\n\telse\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0)).r;\n\t\n\tvec3 grain = (hash23((UV * 10.0) + vec2(TIME * 0.01)) * grain_intensity) - (grain_intensity * 0.5);\n\tgrain = vec3(grain_sample) * grain_intensity;\n\tgrain *= grain;\n\t\n\t\/\/ Final computation.\n\tvec3 result = screen_color;\n\tresult = aberration;\n\tresult += red_blur * red_blur_intensity * red_blur_color;\n\tresult += blue_blur * blue_blur_intensity * blue_blur_color * (1.0 - red_blur);\n\tresult += grain * grain;\n\tresult = mix(result, vec3(1.0), luminance(result) * aberration_yellow_reduction);\n\tresult = mix(result, vec3(0.0), (1.0 - grain) * 0.3);\n\t\n\tCOLOR = vec4(clamp(result, 0.0, 1.0), 1.0);\n}\n\n```","comments":2,"comments-timestamp":"2026-04-22T19:55:32Z","created":"2026-04-22T17:59:27Z","files":[],"files-timestamp":0,"id":431663,"love":23,"love-timestamp":"2026-04-28T22:11:09Z","meta":[],"modified":"2026-04-28T22:11:09Z","name":"Rendering breakdown","node-timestamp":"2026-04-22T18:18:28Z","parent":429496,"parents":[1,5,9,424249,429496],"path":"\/events\/ludum-dare\/59\/kuiper-space-rescue\/rendering-breakdown","published":"2026-04-22T18:16:41Z","scope":"public","slug":"rendering-breakdown","subsubtype":"","subtype":"","type":"post","version":1369777},"node_metadata":{"n_key":"431663","n_urlkey":"312149","n_parent":"429496","n_path":"\/events\/ludum-dare\/59\/kuiper-space-rescue\/rendering-breakdown","n_slug":"rendering-breakdown","n_type":"post","n_subtype":"","n_subsubtype":"","n_author":"81341","n_created":"1776880767","n_modified":"1777414269","n_version":"1369777","n_status":"WAYBACK"},"source_url":"https:\/\/ldjam.com\/events\/ludum-dare\/59\/kuiper-space-rescue\/rendering-breakdown","text":"Hi there! :satellite: \n\nPeople seem to enjoy [**Kuiper Space Rescue**](https:\/\/ldjam.com\/events\/ludum-dare\/59\/kuiper-space-rescue) aesthetics and atmosphere a lot, so I thought I'd share how the rendering was made! Nothing very complex, but hopefully this will give some people ideas or inspiration!\nThe breakdown is about how to go from the left screenshot to the right one:\n\n![before after.png](\/\/\/raw\/dbd\/31\/z\/72542.png)\n\nThe effect looks like this in game:\n\n![kuiper-ezgif.com-optimize (1).gif](\/\/\/raw\/dbd\/31\/z\/72543.gif)\n\n---\n\nReference image:\n\n![ref.gif](\/\/\/raw\/dbd\/31\/z\/72544.gif)\n\nThe idea was not to copy it perfectly, simply to have a starting point to write the shader.\n\nThe game has been made with **Godot**, so the post process setup is a simple CanvasLayer node containing a ColorRect rendered above the game. On this ColorRect is attached a material with the post process shader this breakdown is about.\n\nThe very first thing I've done is adding a bit of **chromatic aberration**, making the image a bit less clean, and most importantly splitting the image into a **yellow** and a **red** layer, the yellow being rendered on top for the global tint.\n\n![aberration.gif](\/\/\/raw\/dbd\/31\/z\/72545.gif)\n\n![ksr_aberration.png](\/\/\/raw\/dbd\/31\/z\/72546.png)\n\nNext effect is adding more red. On the reference, the red color is more diffuse than the yellow, but always near it. A **blur function** is a perfect fit for this. Cool thing is, it does not really matter what blur function is used, since the effect tries to mimic an old machine screen, even a low quality blur can work perfectly fine.\n\n![ksr_redblur.png](\/\/\/raw\/dbd\/31\/z\/72548.png)\n\nThen, the **blueish** color comes in. On the reference it looks like it's leaking horizontally, which again is a nice fit for a blur, this time a **directional blur**. I went for a blur going in 5 directions instead of a horizontal one as we thought it looked better.\nHere's a gif showing the blur parameters in action:\n\n![blueblur-ezgif.com-optimize.gif](\/\/\/raw\/dbd\/31\/z\/7254a.gif)\n\n![ksr_blueblur.png](\/\/\/raw\/dbd\/31\/z\/7254b.png)\n\nThen, a mandatory **grain** effect is added on top of everything.\n\n![ksr_grain.png](\/\/\/raw\/dbd\/31\/z\/7254d.png)\n\nAdding the grain to the rest looks like this:\n\n![ksr_sum.png](\/\/\/raw\/dbd\/31\/z\/7254e.png)\n\nThe rest of the shader is a very light color correction and another grain pass added on top to make the yellow part of the game a bit more dirty.\n\n![ksr_final.png](\/\/\/raw\/dbd\/31\/z\/7254f.png)\n\n---\n\nAnd that's it!\nWhat's great with the effect was that, any asset drawn in pure white looked good once added to the scene. The logo of the game was even exported directly from there:\n\n![image (2).png](\/\/\/raw\/dbd\/31\/z\/72557.png)\n\nThanks for reading! :v: \n\nLast thing to do is to share the shader code! Do not consider it a perfectly written shader, it's still a game jam implementation, but if there's anything you want to take from it, there you go!\n\n```\nshader_type canvas_item;\n\nuniform sampler2D screen_texture: hint_screen_texture;\n\ngroup_uniforms Aberration;\nuniform float aberration_offset: hint_range(0.0, 1.0) = 0.01;\nuniform float aberration_yellow_reduction: hint_range(0.0, 1.0) = 0.1;\n\ngroup_uniforms RedBlur;\nuniform float red_blurriness: hint_range(0, 128, 1) = 64;\nuniform int red_iterations: hint_range(0, 128, 1) = 64;\nuniform int red_quality: hint_range(0, 128, 1) = 32;\nuniform float red_blur_intensity: hint_range(0.0, 10.0) = 3.0;\nuniform vec3 red_blur_color: source_color = vec3(1.0, 0.0, 0.0);\n\ngroup_uniforms BlueBlur;\nuniform float blue_blur_blurriness: hint_range(0, 128, 1) = 8;\nuniform int blue_blur_iterations: hint_range(0, 128, 1) = 8;\nuniform int blue_blur_quality: hint_range(0, 128, 1) = 8;\nuniform float blue_blur_intensity: hint_range(0.0, 10.0) = 1.0;\nuniform vec3 blue_blur_color: source_color = vec3(0.0, 0.0, 1.0);\n\ngroup_uniforms Grain;\nuniform sampler2D grain_texture: repeat_enable;\nuniform float grain_speed: hint_range(0.0, 10.0) = 0.1;\nuniform float grain_intensity: hint_range(0.0, 1.0) = 0.1;\n\nvec3 hash23(vec2 input)\n{\n    float a = dot(input.xyx, vec3(127.1, 311.7, 74.7));\n    float b = dot(input.yxx, vec3(269.5, 183.3, 246.1));\n    float c = dot(input.xyy, vec3(113.5, 271.9, 124.6));\n    return fract(sin(vec3(a, b, c)) * 43758.5453123);\n}\n\nvec4 texture_xorgaussian(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, int iterations, int quality)\n{\n\tvec2 radius = blurriness \/ (1.0 \/ pixel_size).xy;\n\tvec4 blurred_tex = texture(tex, uv);\n\t\n\tfor (float d = 0.0; d < TAU; d += TAU \/ float(iterations))\n\t{\n\t\tfor (float i = 1.0 \/ float(quality); i <= 1.0; i += 1.0 \/ float(quality))\n\t\t{\n\t\t\tvec2 directions = uv + vec2(cos(d), sin(d)) * radius * i;\n\t\t\tblurred_tex += texture(tex, directions);\n\t\t}\n\t}\n\t\n\tblurred_tex \/= float(quality) * float(iterations) + 1.0;\n\treturn blurred_tex;\n}\n\nfloat luminance(vec3 color)\n{\n    const vec3 w = vec3(0.2125, 0.7154, 0.0721);\n    return dot(color, w);\n}\n\nvoid fragment()\n{\n\tvec3 screen_color = texture(screen_texture, SCREEN_UV).rgb;\n\n\t\/\/ Red blur.\n    vec3 red_blur = texture_xorgaussian(screen_texture, SCREEN_UV, SCREEN_PIXEL_SIZE, red_blurriness, red_iterations, red_quality).rgb;\n\tred_blur = clamp(red_blur, 0.0, 1.0);\n\t\n\t\/\/ Blue blur.\n\tvec3 blue_blur = texture_xorgaussian(screen_texture, SCREEN_UV, SCREEN_PIXEL_SIZE, blue_blur_blurriness, blue_blur_iterations, blue_blur_quality).rgb;\n\tblue_blur = clamp(blue_blur, 0.0, 1.0);\n\t\n\t\/\/ Aberration.\n\tvec2 uv_offset = vec2(aberration_offset, 0.0);\n\tvec4 aberration_right = texture(screen_texture, SCREEN_UV + uv_offset);\n\tvec4 aberration_left = texture(screen_texture, SCREEN_UV - uv_offset);\n\tvec3 aberration = vec3((aberration_left.x + aberration_right.x) * 0.8, aberration_right.y * 1.0, 0.0);\n\taberration = clamp(aberration, 0.0, 1.0);\n\t\n\t\/\/ Grain.\n\tfloat grain_time = fract(TIME * grain_speed);\n\tfloat grain_sample = 0.0;\n\tif (grain_time < 0.333f)\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0) + vec2(0.2, 0.2)).r;\n\telse if (grain_time < 0.666f)\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0) + vec2(0.3, 0.1)).r;\n\telse\n\t\tgrain_sample = texture(grain_texture, (UV * 200.0)).r;\n\t\n\tvec3 grain = (hash23((UV * 10.0) + vec2(TIME * 0.01)) * grain_intensity) - (grain_intensity * 0.5);\n\tgrain = vec3(grain_sample) * grain_intensity;\n\tgrain *= grain;\n\t\n\t\/\/ Final computation.\n\tvec3 result = screen_color;\n\tresult = aberration;\n\tresult += red_blur * red_blur_intensity * red_blur_color;\n\tresult += blue_blur * blue_blur_intensity * blue_blur_color * (1.0 - red_blur);\n\tresult += grain * grain;\n\tresult = mix(result, vec3(1.0), luminance(result) * aberration_yellow_reduction);\n\tresult = mix(result, vec3(0.0), (1.0 - grain) * 0.3);\n\t\n\tCOLOR = vec4(clamp(result, 0.0, 1.0), 1.0);\n}\n\n```","title":"Rendering breakdown","wayback_source":[]}