import Phaser from 'phaser'; const fragShader = ` precision mediump float; uniform sampler2D uMainSampler; uniform vec2 resolution; uniform float time; varying vec2 outTexCoord; #define PI 3.14159265359 vec4 permute(vec4 t) { return mod(((t * 34.0) + 1.0) * t, 289.0); } float noise3d(vec3 p) { vec3 a = floor(p); vec3 d = p - a; d = d * d * (3.0 - 2.0 * d); vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); vec4 k1 = permute(b.xyxy); vec4 k2 = permute(k1.xyxy + b.zzww); vec4 c = k2 + a.zzzz; vec4 k3 = permute(c); vec4 k4 = permute(c + 1.0); vec4 o1 = fract(k3 * (1.0 / 41.0)); vec4 o2 = fract(k4 * (1.0 / 41.0)); vec4 o3_interp_z = o2 * d.z + o1 * (1.0 - d.z); vec2 o4_interp_xy = o3_interp_z.yw * d.x + o3_interp_z.xz * (1.0 - d.x); return o4_interp_xy.y * d.y + o4_interp_xy.x * (1.0 - d.y); } void main() { float brightness = 2.5; float red_balance = 1.0; float green_balance = 0.85; float blue_balance = 1.0; float phosphorWidth = 2.50; float phosphorHeight = 4.50; float internalHorizontalGap = 1.0; float columnGap = 0.2; float verticalCellGap = 0.2; float phosphorPower = 0.9; float cell_noise_variation_amount = 0.025; float cell_noise_scale_xy = 240.0; float cell_noise_speed = 24.0; float curvature_amount = 0.0; vec2 fragCoord = gl_FragCoord.xy; vec2 uv = outTexCoord; vec2 centered_uv_output = uv - 0.5; float r = length(centered_uv_output); float distort_factor = 1.0 + curvature_amount * r * r; vec2 centered_uv_source = centered_uv_output * distort_factor; vec2 source_uv = centered_uv_source + 0.5; vec2 fragCoord_warped = source_uv * resolution; bool is_on_original_flat_screen = source_uv.x >= 0.0 && source_uv.x <= 1.0 && source_uv.y >= 0.0 && source_uv.y <= 1.0; if (!is_on_original_flat_screen) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; } float fullCellWidth = 3.0 * phosphorWidth + 3.0 * internalHorizontalGap + columnGap; float fullRowHeight = phosphorHeight + verticalCellGap; float logical_cell_index_x = floor(fragCoord_warped.x / fullCellWidth); float shift_y_offset = 0.0; if (mod(logical_cell_index_x, 2.0) != 0.0) { shift_y_offset = fullRowHeight / 2.0; } float effective_y_warped = fragCoord_warped.y + shift_y_offset; float logical_row_index = floor(effective_y_warped / fullRowHeight); float uv_cell_x = mod(fragCoord_warped.x, fullCellWidth); if (uv_cell_x < 0.0) { uv_cell_x += fullCellWidth; } float uv_row_y = mod(effective_y_warped, fullRowHeight); if (uv_row_y < 0.0) { uv_row_y += fullRowHeight; } vec3 video_color = texture2D(uMainSampler, source_uv).rgb; video_color.r *= red_balance; video_color.g *= green_balance; video_color.b *= blue_balance; vec3 final_color = vec3(0.0); bool in_column_gap = uv_cell_x >= (3.0 * phosphorWidth + 3.0 * internalHorizontalGap); bool in_vertical_gap = uv_row_y >= phosphorHeight; if (!in_column_gap && !in_vertical_gap) { float uv_cell_x_within_block = uv_cell_x; vec3 phosphor_base_color = vec3(0.0); float video_component_intensity = 0.0; float current_phosphor_startX_in_block = -1.0; float current_x_tracker = 0.0; if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) { phosphor_base_color = vec3(1.0, 0.0, 0.0); video_component_intensity = video_color.r; current_phosphor_startX_in_block = current_x_tracker; } current_x_tracker += phosphorWidth + internalHorizontalGap; if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) { phosphor_base_color = vec3(0.0, 1.0, 0.0); video_component_intensity = video_color.g; current_phosphor_startX_in_block = current_x_tracker; } current_x_tracker += phosphorWidth + internalHorizontalGap; if (uv_cell_x_within_block >= current_x_tracker && uv_cell_x_within_block < current_x_tracker + phosphorWidth) { phosphor_base_color = vec3(0.0, 0.0, 1.0); video_component_intensity = video_color.b; current_phosphor_startX_in_block = current_x_tracker; } if (current_phosphor_startX_in_block >= 0.0) { float x_in_phosphor = (uv_cell_x_within_block - current_phosphor_startX_in_block) / phosphorWidth; float horizontal_intensity_factor = pow(sin(x_in_phosphor * PI), phosphorPower); float y_in_phosphor_band = uv_row_y / phosphorHeight; float vertical_intensity_factor = (phosphorHeight > 0.0) ? pow(sin(y_in_phosphor_band * PI), phosphorPower) : 1.0; float total_intensity_factor = horizontal_intensity_factor * vertical_intensity_factor; final_color = phosphor_base_color * video_component_intensity * total_intensity_factor; } } vec3 noise_pos = vec3(logical_cell_index_x * cell_noise_scale_xy, logical_row_index * cell_noise_scale_xy, time * cell_noise_speed); vec3 cell_noise_rgb; cell_noise_rgb.r = noise3d(noise_pos); cell_noise_rgb.g = noise3d(noise_pos + vec3(19.0, 0.0, 0.0)); cell_noise_rgb.b = noise3d(noise_pos + vec3(0.0, 13.0, 0.0)); cell_noise_rgb = cell_noise_rgb * 2.0 - 1.0; final_color += cell_noise_rgb * cell_noise_variation_amount; final_color *= brightness; float edge_darken_strength = 0.1; float vignette_factor = 1.0 - dot(centered_uv_output, centered_uv_output) * edge_darken_strength * 2.0; vignette_factor = clamp(vignette_factor, 0.0, 1.0); final_color *= vignette_factor; final_color = clamp(final_color, 0.0, 1.0); gl_FragColor = vec4(final_color, 1.0); } `; export default class TrinitronPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { constructor(game) { super({ name: 'TrinitronPipeline', game: game, renderTarget: true, fragShader: fragShader, uniforms: [ 'uMainSampler', 'resolution', 'time' ] }); } onPreRender() { // Use the actual canvas display size (after scaling), not the game resolution const canvas = this.game.canvas; const displayWidth = canvas.width; const displayHeight = canvas.height; this.set2f('resolution', displayWidth, displayHeight); this.set1f('time', this.game.loop.time / 1000); } }