Let's talk about technology this time. In this article I will tell and show you how to create a shader in Unity for styling graphics for old CRTs. Such a shader is suitable for pixel art and for stylizing an image as an ancient technique. They should not be abused, but sometimes it is possible to use it in place. (Specifically, I’ll clarify - I don’t suggest using this effect all the time. But, for example, in splash screens - it may come to a place).

And at once I will make a reservation - I do not have a deep understanding of shaders, and from the reader I expect even less. So I will write on the basis that you know nothing about shaders, or almost nothing. And yes, I will try to explain to you the very bases of the work of shaders, so if you do not know anything about them - welcome!
')
What are shaders and how do they work?
Here you need to know the following: a shader is a small program running on a video card processor for each vertex (vertex shaders) and for each pixel drawn (pixel or “fragmental” shaders).
In fact, even a simple stretching of a texture onto a triangle is a shader. In such a shader, the vertex part of it is involved in calculating the vertices of the triangle, and the pixel part - directly drawing the texture pixels. And, accordingly - the shader is called to draw each pixel. With that, they can work in parallel.
An important remark. Each shader reads the input parameters and outputs the output. With that, it happens in any order. In modern maps, a large number of shader streams that can be launched in parallel. That is, when one shader processes a pixel by coordinates (0, 0), the other at the same time can calculate a pixel by coordinates (10, 10).
Thus, the shader when processing a pixel in (0, 1) does not know (and does not have access to) the result of processing a pixel (0, 0). It can only refer to the original value. Therefore, if you need to apply several mutually dependent effects sequentially, you will most likely have to write several shaders.
In Unity, you can use different shader languages, but I advise CG, since It compiles well in both OpenGL and DirectX. Accordingly, we do not need to write two different shaders.
Now, the next moment - to implement the post-processing of the image (and that is what we planned) - we need to write not a shader for the sprites themselves, but a general shader for the entire screen. More precisely, the render will be sent first to the texture, and this texture already using the shader will be drawn on the screen. And yes, for this we need the Pro version of the Unity package.
So fight
The first thing we need is to learn how to create a shader for the entire camera, which does not change anything.
To do this, create a new shader (file with the extension .shader) and copy this disc there:
Shader "Custom/CRTShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" #pragma target 3.0 struct v2f { float4 pos : POSITION; float2 uv : TEXCOORD0; }; uniform sampler2D _MainTex; v2f vert(appdata_img v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord); return o; } half4 frag(v2f i): COLOR { half4 color = tex2D(_MainTex, i.uv); return color; } ENDCG } } FallBack "Diffuse" }
Let me explain the main points:
- Properties - describes the shader input parameters (parameters coming from the outside). At the moment it is only a texture.
- vert - vertex (vertex) shader, frag - pixel (fragmental)
- struct v2f - describes the data structure passed from the vertex to the pixel shaders
- uniform - creates a link from the shader language to the same parameter (s) from claim 1
- In our example, the vertex shader performs a matrix operation to calculate the vertex coordinates and coordinates for the texture. Let's take it for the magic that works;)
- In the same example, the pixel shader reads the texture according to the coordinates obtained from the vertex shader (command tex2D ) and outputs the resulting color , which will go to the drawing.
- In a shader language, multicomponent structures are often needed. For example, 3 coordinates or 4 color components. For their description, types like float2 are used (meaning a structure of two floats) or, for example, int4. Components can be accessed through the point .x .y .z .w or .r .g .b .a
Left just a little bit. We need to apply a shader to the camera.
To do this, create another C # control script:
Hidden text using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class TVShader : MonoBehaviour { public Shader shader; private Material _material; protected Material material { get { if (_material == null) { _material = new Material(shader); _material.hideFlags = HideFlags.HideAndDontSave; } return _material; } } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (shader == null) return; Material mat = material; Graphics.Blit(source, destination, mat); } void OnDisable() { if (_material) { DestroyImmediate(_material); } } }
It remains only to throw it on the camera and in the “shader” field indicate our shader.
How to understand that this thing works? Try writing something like
color.r = 0;
in the shader before "
return color
"
color.r = 0;
and if everything is ok, then you get a picture without red.
So, the shaders are sorted out.
Getting to the effect.
What do we want to achieve? To begin with, let's try to realize the effect when the image consists of colored RGB pixels. That is so:

How to do is pretty obvious. It is necessary with a cycle of 3 pixels to leave only the R, G and B component of each pixel on the screen.
Task number 1 is to get the screen coordinates of the current point in the pixel shader.
To do this, we will need to calculate something in the vertex shader and forward this matter to the pixel one. As mentioned above, for the exchange of data between the vertex and pixel shaders, there is a
v2f
construction in which there are currently two fields,
pos
and
uv
. Add there:
float4 scr_pos : TEXCOORD1;
and also add a line to the vertex shader:
o.scr_pos = ComputeScreenPos(o.pos);
Now in the pixel shader we get the screen coordinate in the range from (0 ... 1). We need pixels. This is also done simply:
float2 ps = i.scr_pos.xy *_ScreenParams.xy / i.scr_pos.w;
Hooray! In
ps
we have pixel coordinates on the screen. Then everything is quite simple. You need to write something like:
int pp = (int)ps.x % 3;
We get something like this:

Two moments are immediately visible - firstly, the effect was very strong, and secondly, the picture became darker. Thank God, having corrected the first - the second will correct also.
I propose to do not rigid division by R / G / B, and in any case, to leave all the components, just in different proportions. That is, in the "red" column, leave 100% R, and about 50% of G and B. And even better, if we can tune this thing.
In fact, our transformation can be done by multiplying the color by a kind of multiplier. To leave only R, we need to multiply
color
by
float4(1, 0, 0, 1)
(the 4th component is alpha, we do not change it). We want to adjust the coefficients. That is, multiply the red bar by (1, k1, k2, 1), the green bar - by (k2, 1, k1, 1) and the blue bar by (k1, k2, 1, 1).
To begin, add a description of the two parameters at the very beginning of the shader:
_VertsColor("Verts fill color", Float) = 0 _VertsColor2("Verts fill color 2", Float) = 0
then write the links:
uniform float _VertsColor; uniform float _VertsColor2;
Now we go to the pixel shader code and manipulate the color:
if (pp == 1) { muls.r = 1; muls.g = _VertsColor2; } else if (pp == 2) { muls.g = 1; muls.b = _VertsColor2; } else { muls.b = 1; muls.r = _VertsColor2; } color = color * muls;
It remains only to learn how to manage these parameters in Unity. Let's write them in our C #:
[Range(0, 1)] public float verts_force = 0.0f; [Range(0, 1)] public float verts_force_2 = 0.0f;
And in the OnRenderImage method add before Graphics.Blit:
mat.SetFloat("_VertsColor", 1-verts_force); mat.SetFloat("_VertsColor2", 1-verts_force_2);
Here I subtract from 1 to make it more visual. The larger the parameter, the stronger the darkening of the column.
If you did everything correctly, then in the Unity inspector when choosing a camera, you should have sliders:

Now look at the effect:

Better, but you still want brightness. Let's add brightness and contrast controls to our shader.
_Contrast("Contrast", Float) = 0 _Br("Brightness", Float) = 0 .... uniform float _Contrast; uniform float _Br; .... color += (_Br / 255); color = color - _Contrast * (color - 1.0) * color *(color - 0.5);
C # script:
[Range(-3, 20)] public float contrast = 0.0f; [Range(-200, 200)] public float brightness = 0.0f; ... mat.SetFloat("_Contrast", contrast); mat.SetFloat("_Br", brightness);
Result:

(values ​​of contrast = 2.1, brightness = 27)
Now let's implement the scanlines. Everything is simple here. Every 3rd row needs to be dimmed.
if ((int)ps.y % 3 == 0) muls *= float4(_ScansColor, _ScansColor, _ScansColor, 1);

And the final touch is the Bloom effect. Take such a shader can, for example,
here .
Done! We get the picture from the top of the article!

Yes, and of course - this shader will look best on a triple pixel, as in my examples.
UPD : As suggested below in the comments, you can solve this whole problem simply by multiplying one texture by another. Those. according to the specified parameters of the intensity of the bands, we render the texture pattern, and then perform the multiplication. So it will be faster in terms of speed, but the goal of this article was not to write the optimal shader, but to show the general principle.
All articles in the series:- Idea, vision, choice of setting, platform, distribution model, etc.
- Shaders for styling images under the CRT / LCD
- We fasten the scripting language to Unity (UniLua)
- Shader for fade in on the palette (a la NES)
- Subtotal (prototype)
- Let's talk about the indie games
- 2D animations in Unity ("as in flash")
- Visual scripting of cut scenes in Unity (uScript)