How do i make a Compression Artifact post-process like Imperfecter?

I’m pretty new to game dev and working on a VHS-style horror game. I’m trying to find a free post-processing effect to get those retro compression artifacts (like this video “https://www.youtube.com/watch?v=KopGQLysx2Y”).

Everything I’m finding online seems to be paid (like Imperfecter). Does anyone know of any good free alternatives or tutorials on how to achieve this effect?

Hello there @hpprhpd!

Looking around the community, the following guide covers how to create a basic CRT/VHS effect with blueprints, you can check it here:

Alternatively, there’s a VHS effect packed in FAB. It’s paid content, but it does include a demo, which could be useful for experimentation:

I just want the Artifact Compression effect (the first doesn’t have), my budget is low (20<), in my country the price is 90$

1 Like

And yah the custom script was written by Ai, im sorry but noway im going to pull this off so ya

I know i#m late but i manege to pull this of by uisng a custom script

setup like this heres the script

// ============================================================
// NATURAL LOW-BITRATE / BLOCKY COLOR COMPRESSION
// UE 5.6 CUSTOM NODE
//
// INPUT:
// SceneColor : Float3
//
// OUTPUT:
// Float3
//
// Kein CRT
// Keine Scanlines
// Kein Chroma-Bleeding
// Weniger harte 8x8-Kompression
// Blockige Farbquantisierung bleibt deutlich sichtbar
// ============================================================

float2 UV = GetDefaultSceneTextureUV(Parameters, 14);
float2 InvRes = View.ViewSizeAndInvSize.zw;
float2 Pixel = UV * View.ViewSizeAndInvSize.xy;

float3 Original = saturate(SceneColor);

// ============================================================
// 8x8 COMPRESSION BLOCK
// ============================================================

float2 Block =
floor(Pixel / 8.0);

float2 BlockUV =
(Block * 8.0 + 4.0) * InvRes;

// ============================================================
// BLOCK SAMPLES
// ============================================================

float3 B0 =
SceneTextureLookup(
BlockUV,
14,
false
).rgb;

float3 B1 =
SceneTextureLookup(
BlockUV + float2(2.0 * InvRes.x, 0.0),
14,
false
).rgb;

float3 B2 =
SceneTextureLookup(
BlockUV - float2(2.0 * InvRes.x, 0.0),
14,
false
).rgb;

float3 B3 =
SceneTextureLookup(
BlockUV + float2(0.0, 2.0 * InvRes.y),
14,
false
).rgb;

float3 B4 =
SceneTextureLookup(
BlockUV - float2(0.0, 2.0 * InvRes.y),
14,
false
).rgb;

// Block color
float3 BlockColor =
B0 * 0.40

  • B1 * 0.15
  • B2 * 0.15
  • B3 * 0.15
  • B4 * 0.15;

// ============================================================
// RGB → YCbCr
// ============================================================

float Y =
dot(
Original,
float3(
0.299,
0.587,
0.114
)
);

float Cb =
dot(
Original,
float3(
-0.168736,
-0.331264,
0.5
)
);

float Cr =
dot(
Original,
float3(
0.5,
-0.418688,
-0.081312
)
);

// ============================================================
// BLOCK YCbCr
// ============================================================

float BY =
dot(
BlockColor,
float3(
0.299,
0.587,
0.114
)
);

float BCb =
dot(
BlockColor,
float3(
-0.168736,
-0.331264,
0.5
)
);

float BCr =
dot(
BlockColor,
float3(
0.5,
-0.418688,
-0.081312
)
);

// ============================================================
// BLOCK RANDOMNESS
// ============================================================

float BlockHash =
frac(
sin(
dot(
Block,
float2(
127.1,
311.7
)
)
)
* 43758.5453
);

// unterschiedliche Kompression pro Block
float Damage =
lerp(
0.35,
1.0,
BlockHash
);

// ============================================================
// CHROMA QUANTIZATION
// ============================================================

float ChromaLevels =
lerp(
25.0,
14.0,
Damage
);

float QCb =
floor(
BCb * ChromaLevels + 0.5
)
/ ChromaLevels;

float QCr =
floor(
BCr * ChromaLevels + 0.5
)
/ ChromaLevels;

// ============================================================
// LUMA QUANTIZATION
// ============================================================

float LumaLevels =
lerp(
58.0,
34.0,
Damage
);

float QY =
floor(
BY * LumaLevels + 0.5
)
/ LumaLevels;

// ============================================================
// QUANTIZATION ERRORS
// ============================================================

float CbError =
QCb - Cb;

float CrError =
QCr - Cr;

float YError =
QY - Y;

// ============================================================
// EDGE DETECTION
// ============================================================

float3 Left =
SceneTextureLookup(
UV - float2(2.0 * InvRes.x, 0.0),
14,
false
).rgb;

float3 Right =
SceneTextureLookup(
UV + float2(2.0 * InvRes.x, 0.0),
14,
false
).rgb;

float3 Up =
SceneTextureLookup(
UV - float2(0.0, 2.0 * InvRes.y),
14,
false
).rgb;

float3 Down =
SceneTextureLookup(
UV + float2(0.0, 2.0 * InvRes.y),
14,
false
).rgb;

float Edge =
length(Right - Left)

  • length(Down - Up);

Edge =
saturate(
Edge * 2.0
);

// ============================================================
// LOCAL BLOCK VARIATION
// ============================================================

float LocalVariation =
length(B0 - B1)

  • length(B0 - B2)
  • length(B0 - B3)
  • length(B0 - B4);

LocalVariation =
saturate(
LocalVariation * 2.0
);

// ============================================================
// COMPRESSION MASK
//
// Bewusst reduziert:
// weniger sichtbare harte 8x8-Blöcke,
// aber immer noch genug für Low-Bitrate-Look.
// ============================================================

float Artifact =
saturate(
0.20

  • LocalVariation * 0.38
  • Edge * 0.30
    );

Artifact *= Damage;

// ============================================================
// BLOCK COLOR RECONSTRUCTION
// ============================================================

float3 BlockCompressed;

BlockCompressed.r =
Y

  • 1.402 *
    (Cr + CrError);

BlockCompressed.g =
Y

  • 0.344136 *
    (Cb + CbError)
  • 0.714136 *
    (Cr + CrError);

BlockCompressed.b =
Y

  • 1.772 *
    (Cb + CbError);

// ============================================================
// APPLY BLOCK COLOR ERROR
// ============================================================

float3 ResultColor =
lerp(
Original,
BlockCompressed,
Artifact * 0.62
);

// ============================================================
// BLOCKY COLOR ERROR
//
// Kleine farbige Quantisierungsfehler.
// ============================================================

float ColorError =
(BlockHash - 0.5)
* Artifact
* 0.065;

ResultColor.r +=
ColorError;

ResultColor.g -=
ColorError * 0.45;

ResultColor.b +=
ColorError * 0.75;

// ============================================================
// LUMA COMPRESSION
// ============================================================

ResultColor +=
YError
* Artifact
* 0.30;

// ============================================================
// 16x16 SECONDARY COMPRESSION PATCHES
//
// Größere unregelmäßige Farbflächen.
// ============================================================

float2 BigBlock =
floor(
Pixel / 16.0
);

float BigHash =
frac(
sin(
dot(
BigBlock,
float2(
269.5,
183.3
)
)
)
* 43758.5453
);

float Patch =
smoothstep(
0.48,
0.78,
BigHash
);

float PatchError =
(BigHash - 0.5)
* Patch
* 0.020;

ResultColor.r +=
PatchError;

ResultColor.g -=
PatchError * 0.45;

ResultColor.b +=
PatchError * 0.65;

// ============================================================
// SUBTLE BLOCK EDGE RINGING
// ============================================================

float3 FarLeft =
SceneTextureLookup(
UV - float2(5.0 * InvRes.x, 0.0),
14,
false
).rgb;

float3 FarRight =
SceneTextureLookup(
UV + float2(5.0 * InvRes.x, 0.0),
14,
false
).rgb;

float FarYL =
dot(
FarLeft,
float3(
0.299,
0.587,
0.114
)
);

float FarYR =
dot(
FarRight,
float3(
0.299,
0.587,
0.114
)
);

float Ring =
(
FarYL

  • FarYR
    )
    * 0.5
  • Y;

Ring =
clamp(
Ring,
-0.035,
0.035
);

ResultColor +=
Ring
* Edge
* 0.28;

// ============================================================
// BLOCKY POSTERIZATION
//
// Absichtlich NICHT glatt.
// Aber wesentlich dezenter als die Kompressionsfehler.
// ============================================================

float3 QuantizedColor =
floor(
saturate(ResultColor)
* 42.0

  • 0.5
    )
    / 42.0;

ResultColor =
lerp(
ResultColor,
QuantizedColor,
0.22
);

// ============================================================
// FINAL COMPRESSION MIX
// ============================================================

float CompressionStrength =
0.86;

float3 FinalResult =
lerp(
Original,
ResultColor,
CompressionStrength
);

// ============================================================
// OUTPUT
// ============================================================

return saturate(FinalResult); make sure to make input scene color for custom node

in the end it look smtng like this hope tht helps.

PS:its the artifacts are more visible in dark areas.