/* Feel free to do anything you want with this code. This shader uses "runes" code by FabriceNeyret2 (https://www.shadertoy.com/view/4ltyDM) which is based on "runes" by otaviogood (https://shadertoy.com/view/MsXSRn). These random runes look good as matrix symbols and have acceptable performance. @pkazmier modified this shader to work in Ghostty. */ const int ITERATIONS = 40; //use less value if you need more performance const float SPEED = .5; const float STRIP_CHARS_MIN = 7.; const float STRIP_CHARS_MAX = 40.; const float STRIP_CHAR_HEIGHT = 0.15; const float STRIP_CHAR_WIDTH = 0.10; const float ZCELL_SIZE = 1. * (STRIP_CHAR_HEIGHT * STRIP_CHARS_MAX); //the multiplier can't be less than 1. const float XYCELL_SIZE = 12. * STRIP_CHAR_WIDTH; //the multiplier can't be less than 1. const int BLOCK_SIZE = 10; //in cells const int BLOCK_GAP = 2; //in cells const float WALK_SPEED = 0.5 * XYCELL_SIZE; const float BLOCKS_BEFORE_TURN = 3.; const float PI = 3.14159265359; // ---- random ---- float hash(float v) { return fract(sin(v)*43758.5453123); } float hash(vec2 v) { return hash(dot(v, vec2(5.3983, 5.4427))); } vec2 hash2(vec2 v) { v = vec2(v * mat2(127.1, 311.7, 269.5, 183.3)); return fract(sin(v)*43758.5453123); } vec4 hash4(vec2 v) { vec4 p = vec4(v * mat4x2( 127.1, 311.7, 269.5, 183.3, 113.5, 271.9, 246.1, 124.6 )); return fract(sin(p)*43758.5453123); } vec4 hash4(vec3 v) { vec4 p = vec4(v * mat4x3( 127.1, 311.7, 74.7, 269.5, 183.3, 246.1, 113.5, 271.9, 124.6, 271.9, 269.5, 311.7 ) ); return fract(sin(p)*43758.5453123); } // ---- symbols ---- // Slightly modified version of "runes" by FabriceNeyret2 - https://www.shadertoy.com/view/4ltyDM // Which is based on "runes" by otaviogood - https://shadertoy.com/view/MsXSRn float rune_line(vec2 p, vec2 a, vec2 b) { // from https://www.shadertoy.com/view/4dcfW8 p -= a, b -= a; float h = clamp(dot(p, b) / dot(b, b), 0., 1.); // proj coord on line return length(p - b * h); // dist to segment } float rune(vec2 U, vec2 seed, float highlight) { float d = 1e5; for (int i = 0; i < 4; i++) // number of strokes { vec4 pos = hash4(seed); seed += 1.; // each rune touches the edge of its box on all 4 sides if (i == 0) pos.y = .0; if (i == 1) pos.x = .999; if (i == 2) pos.x = .0; if (i == 3) pos.y = .999; // snap the random line endpoints to a grid 2x3 vec4 snaps = vec4(2, 3, 2, 3); pos = ( floor(pos * snaps) + .5) / snaps; if (pos.xy != pos.zw) //filter out single points (when start and end are the same) d = min(d, rune_line(U, pos.xy, pos.zw + .001) ); // closest line } return smoothstep(0.1, 0., d) + highlight*smoothstep(0.4, 0., d); } float random_char(vec2 outer, vec2 inner, float highlight) { vec2 seed = vec2(dot(outer, vec2(269.5, 183.3)), dot(outer, vec2(113.5, 271.9))); return rune(inner, seed, highlight); } // ---- digital rain ---- // xy - horizontal, z - vertical vec3 rain(vec3 ro3, vec3 rd3, float time) { vec4 result = vec4(0.); // normalized 2d projection vec2 ro2 = vec2(ro3); vec2 rd2 = normalize(vec2(rd3)); // we use formulas `ro3 + rd3 * t3` and `ro2 + rd2 * t2`, `t3_to_t2` is a multiplier to convert t3 to t2 bool prefer_dx = abs(rd2.x) > abs(rd2.y); float t3_to_t2 = prefer_dx ? rd3.x / rd2.x : rd3.y / rd2.y; // at first, horizontal space (xy) is divided into cells (which are columns in 3D) // then each xy-cell is divided into vertical cells (along z) - each of these cells contains one raindrop ivec3 cell_side = ivec3(step(0., rd3)); //for positive rd.x use cell side with higher x (1) as the next side, for negative - with lower x (0), the same for y and z ivec3 cell_shift = ivec3(sign(rd3)); //shift to move to the next cell // move through xy-cells in the ray direction float t2 = 0.; // the ray formula is: ro2 + rd2 * t2, where t2 is positive as the ray has a direction. ivec2 next_cell = ivec2(floor(ro2/XYCELL_SIZE)); //first cell index where ray origin is located for (int i=0; i= t2s && tmin <= t2) { float u = s.x * rd2.y - s.y * rd2.x; //horizontal coord in the matrix strip if (abs(u) < target_rad) { u = (u/target_rad + 1.) / 2.; float z = ro3.z + rd3.z * tmin/t3_to_t2; float v = (z - target_z) / target_length; //vertical coord in the matrix strip if (v >= 0.0 && v < 1.0) { float c = floor(v * chars_count); //symbol index relative to the start of the strip, with addition of char_z_shift it becomes an index relative to the whole cell float q = fract(v * chars_count); vec2 char_hash = hash2(vec2(c+char_z_shift, cell_hash2.x)); if (char_hash.x >= 0.1 || c == 0.) { //10% of missed symbols float time_factor = floor(c == 0. ? time*5.0 : //first symbol is changed fast time*(1.0*cell_hash2.z + //strips are changed sometime with different speed cell_hash2.w*cell_hash2.w*4.*pow(char_hash.y, 4.))); //some symbols in some strips are changed relatively often float a = random_char(vec2(char_hash.x, time_factor), vec2(u,q), max(1., 3. - c/2.)*0.2); //alpha a *= clamp((chars_count - 0.5 - c) / 2., 0., 1.); //tail fade if (a > 0.) { float attenuation = 1. + pow(0.06*tmin/t3_to_t2, 2.); vec3 col = (c == 0. ? vec3(0.67, 1.0, 0.82) : vec3(0.25, 0.80, 0.40)) / attenuation; float a1 = result.a; result.a = a1 + (1. - a1) * a; result.xyz = (result.xyz * a1 + col * (1. - a1) * a) / result.a; if (result.a > 0.98) return result.xyz; } } } } } // not found in this cell - go to next vertical cell zcell += cell_shift.z; } // go to next horizontal cell } return result.xyz * result.a; } // ---- main, camera ---- vec2 rotate(vec2 v, float a) { float s = sin(a); float c = cos(a); mat2 m = mat2(c, -s, s, c); return m * v; } vec3 rotateX(vec3 v, float a) { float s = sin(a); float c = cos(a); return mat3(1.,0.,0.,0.,c,-s,0.,s,c) * v; } vec3 rotateY(vec3 v, float a) { float s = sin(a); float c = cos(a); return mat3(c,0.,-s,0.,1.,0.,s,0.,c) * v; } vec3 rotateZ(vec3 v, float a) { float s = sin(a); float c = cos(a); return mat3(c,-s,0.,s,c,0.,0.,0.,1.) * v; } float smoothstep1(float x) { return smoothstep(0., 1., x); } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { if (STRIP_CHAR_WIDTH > XYCELL_SIZE || STRIP_CHAR_HEIGHT * STRIP_CHARS_MAX > ZCELL_SIZE) { // error fragColor = vec4(1., 0., 0., 1.); return; } vec2 uv = fragCoord.xy / iResolution.xy; float time = iTime * SPEED; const float turn_rad = 0.25 / BLOCKS_BEFORE_TURN; //0 .. 0.5 const float turn_abs_time = (PI/2.*turn_rad) * 1.5; //multiplier different than 1 means a slow down on turns const float turn_time = turn_abs_time / (1. - 2.*turn_rad + turn_abs_time); //0..1, but should be <= 0.5 float level1_size = float(BLOCK_SIZE) * BLOCKS_BEFORE_TURN * XYCELL_SIZE; float level2_size = 4. * level1_size; float gap_size = float(BLOCK_GAP) * XYCELL_SIZE; vec3 ro = vec3(gap_size/2., gap_size/2., 0.); vec3 rd = vec3(uv.x, 2.0, uv.y); float tq = fract(time / (level2_size*4.) * WALK_SPEED); //the whole cycle time counter float t8 = fract(tq*4.); //time counter while walking on one of the four big sides float t1 = fract(t8*8.); //time counter while walking on one of the eight sides of the big side vec2 prev; vec2 dir; if (tq < 0.25) { prev = vec2(0.,0.); dir = vec2(0.,1.); } else if (tq < 0.5) { prev = vec2(0.,1.); dir = vec2(1.,0.); } else if (tq < 0.75) { prev = vec2(1.,1.); dir = vec2(0.,-1.); } else { prev = vec2(1.,0.); dir = vec2(-1.,0.); } float angle = floor(tq * 4.); //0..4 wich means 0..2*PI prev *= 4.; const float first_turn_look_angle = 0.4; const float second_turn_drift_angle = 0.5; const float fifth_turn_drift_angle = 0.25; vec2 turn; float turn_sign = 0.; vec2 dirL = rotate(dir, -PI/2.); vec2 dirR = -dirL; float up_down = 0.; float rotate_on_turns = 1.; float roll_on_turns = 1.; float add_angel = 0.; if (t8 < 0.125) { turn = dirL; //dir = dir; turn_sign = -1.; angle -= first_turn_look_angle * (max(0., t1 - (1. - turn_time*2.)) / turn_time - max(0., t1 - (1. - turn_time)) / turn_time * 2.5); roll_on_turns = 0.; } else if (t8 < 0.250) { prev += dir; turn = dir; dir = dirL; angle -= 1.; turn_sign = 1.; add_angel += first_turn_look_angle*0.5 + (-first_turn_look_angle*0.5+1.0+second_turn_drift_angle)*t1; rotate_on_turns = 0.; roll_on_turns = 0.; } else if (t8 < 0.375) { prev += dir + dirL; turn = dirR; //dir = dir; turn_sign = 1.; add_angel += second_turn_drift_angle*sqrt(1.-t1); //roll_on_turns = 0.; } else if (t8 < 0.5) { prev += dir + dir + dirL; turn = dirR; dir = dirR; angle += 1.; turn_sign = 0.; up_down = sin(t1*PI) * 0.37; } else if (t8 < 0.625) { prev += dir + dir; turn = dir; dir = dirR; angle += 1.; turn_sign = -1.; up_down = sin(-min(1., t1/(1.-turn_time))*PI) * 0.37; } else if (t8 < 0.750) { prev += dir + dir + dirR; turn = dirL; //dir = dir; turn_sign = -1.; add_angel -= (fifth_turn_drift_angle + 1.) * smoothstep1(t1); rotate_on_turns = 0.; roll_on_turns = 0.; } else if (t8 < 0.875) { prev += dir + dir + dir + dirR; turn = dir; dir = dirL; angle -= 1.; turn_sign = 1.; add_angel -= fifth_turn_drift_angle - smoothstep1(t1) * (fifth_turn_drift_angle * 2. + 1.); rotate_on_turns = 0.; roll_on_turns = 0.; } else { prev += dir + dir + dir; turn = dirR; //dir = dir; turn_sign = 1.; angle += fifth_turn_drift_angle * (1.5*min(1., (1.-t1)/turn_time) - 0.5*smoothstep1(1. - min(1.,t1/(1.-turn_time)))); } if (iMouse.x > 10. || iMouse.y > 10.) { vec2 mouse = iMouse.xy / iResolution.xy * 2. - 1.; up_down = -0.7 * mouse.y; angle += mouse.x; rotate_on_turns = 1.; roll_on_turns = 0.; } else { angle += add_angel; } rd = rotateX(rd, up_down); vec2 p; if (turn_sign == 0.) { // move forward p = prev + dir * (turn_rad + 1. * t1); } else if (t1 > (1. - turn_time)) { // turn float tr = (t1 - (1. - turn_time)) / turn_time; vec2 c = prev + dir * (1. - turn_rad) + turn * turn_rad; p = c + turn_rad * rotate(dir, (tr - 1.) * turn_sign * PI/2.); angle += tr * turn_sign * rotate_on_turns; rd = rotateY(rd, sin(tr*turn_sign*PI) * 0.2 * roll_on_turns); //roll } else { // move forward t1 /= (1. - turn_time); p = prev + dir * (turn_rad + (1. - turn_rad*2.) * t1); } rd = rotateZ(rd, angle * PI/2.); ro.xy += level1_size * p; ro += rd * 0.2; rd = normalize(rd); // vec3 col = rain(ro, rd, time); vec3 col = rain(ro, rd, time) * 0.25; // Sample the terminal screen texture including alpha channel vec4 terminalColor = texture(iChannel0, uv); // Combine the matrix effect with the terminal color // vec3 blendedColor = terminalColor.rgb + col; // Make a mask that is 1.0 where the terminal content is not black float mask = 1.2 - step(0.5, dot(terminalColor.rgb, vec3(1.0))); vec3 blendedColor = mix(terminalColor.rgb * 1.2, col, mask); fragColor = vec4(blendedColor, terminalColor.a); }