CRT.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { Fn, float, vec2, vec3, sin, screenUV, mix, clamp, dot, convertToTexture, time, uv, select } from 'three/tsl';
  2. import { circle } from './Shape.js';
  3. /**
  4. * Creates barrel-distorted UV coordinates.
  5. * The center of the screen appears to bulge outward (convex distortion).
  6. *
  7. * @tsl
  8. * @function
  9. * @param {Node<float>} [curvature=0.1] - The amount of curvature (0 = flat, 0.5 = very curved).
  10. * @param {Node<vec2>} [coord=uv()] - The input UV coordinates.
  11. * @return {Node<vec2>} The distorted UV coordinates.
  12. */
  13. export const barrelUV = Fn( ( [ curvature = float( 0.1 ), coord = uv() ] ) => {
  14. // Center UV coordinates (-1 to 1)
  15. const centered = coord.sub( 0.5 ).mul( 2.0 );
  16. // Calculate squared distance from center
  17. const r2 = dot( centered, centered );
  18. // Barrel distortion: push center outward (bulge effect)
  19. const distortion = float( 1.0 ).sub( r2.mul( curvature ) );
  20. // Calculate scale to compensate for edge expansion
  21. // At corners r² = 2, so we scale by the inverse of corner distortion
  22. const cornerDistortion = float( 1.0 ).sub( curvature.mul( 2.0 ) );
  23. // Apply distortion and compensate scale to keep edges aligned
  24. const distorted = centered.div( distortion ).mul( cornerDistortion ).mul( 0.5 ).add( 0.5 );
  25. return distorted;
  26. } );
  27. /**
  28. * Checks if UV coordinates are inside the valid 0-1 range.
  29. * Useful for masking areas inside the distorted screen.
  30. *
  31. * @tsl
  32. * @function
  33. * @param {Node<vec2>} coord - The UV coordinates to check.
  34. * @return {Node<float>} 1.0 if inside bounds, 0.0 if outside.
  35. */
  36. export const barrelMask = Fn( ( [ coord ] ) => {
  37. const outOfBounds = coord.x.lessThan( 0.0 )
  38. .or( coord.x.greaterThan( 1.0 ) )
  39. .or( coord.y.lessThan( 0.0 ) )
  40. .or( coord.y.greaterThan( 1.0 ) );
  41. return select( outOfBounds, float( 0.0 ), float( 1.0 ) );
  42. } );
  43. /**
  44. * Applies color bleeding effect to simulate horizontal color smearing.
  45. * Simulates the analog signal bleeding in CRT displays where colors
  46. * "leak" into adjacent pixels horizontally.
  47. *
  48. * @tsl
  49. * @function
  50. * @param {Node} color - The input texture node.
  51. * @param {Node<float>} [amount=0.002] - The amount of color bleeding (0-0.01).
  52. * @return {Node<vec3>} The color with bleeding effect applied.
  53. */
  54. export const colorBleeding = Fn( ( [ color, amount = float( 0.002 ) ] ) => {
  55. const inputTexture = convertToTexture( color );
  56. // Get the original color
  57. const original = inputTexture.sample( screenUV ).rgb;
  58. // Sample colors from the left (simulating signal trailing)
  59. const left1 = inputTexture.sample( screenUV.sub( vec2( amount, 0.0 ) ) ).rgb;
  60. const left2 = inputTexture.sample( screenUV.sub( vec2( amount.mul( 2.0 ), 0.0 ) ) ).rgb;
  61. const left3 = inputTexture.sample( screenUV.sub( vec2( amount.mul( 3.0 ), 0.0 ) ) ).rgb;
  62. // Red bleeds more (travels further in analog signal)
  63. const bleedR = original.r
  64. .add( left1.r.mul( 0.4 ) )
  65. .add( left2.r.mul( 0.2 ) )
  66. .add( left3.r.mul( 0.1 ) );
  67. // Green bleeds medium
  68. const bleedG = original.g
  69. .add( left1.g.mul( 0.25 ) )
  70. .add( left2.g.mul( 0.1 ) );
  71. // Blue bleeds least
  72. const bleedB = original.b
  73. .add( left1.b.mul( 0.15 ) );
  74. // Normalize and clamp
  75. const r = clamp( bleedR.div( 1.7 ), 0.0, 1.0 );
  76. const g = clamp( bleedG.div( 1.35 ), 0.0, 1.0 );
  77. const b = clamp( bleedB.div( 1.15 ), 0.0, 1.0 );
  78. return vec3( r, g, b );
  79. } );
  80. /**
  81. * Applies scanline effect to simulate CRT monitor horizontal lines with animation.
  82. *
  83. * @tsl
  84. * @function
  85. * @param {Node<vec3>} color - The input color.
  86. * @param {Node<float>} [intensity=0.3] - The intensity of the scanlines (0-1).
  87. * @param {Node<float>} [count=240] - The number of scanlines (typically matches vertical resolution).
  88. * @param {Node<float>} [speed=0.0] - The scroll speed of scanlines (0 = static, 1 = normal CRT roll).
  89. * @param {Node<vec2>} [coord=uv()] - The UV coordinates to use for scanlines.
  90. * @return {Node<vec3>} The color with scanlines applied.
  91. */
  92. export const scanlines = Fn( ( [ color, intensity = float( 0.3 ), count = float( 240.0 ), speed = float( 0.0 ), coord = uv() ] ) => {
  93. // Animate scanlines scrolling down (like CRT vertical sync roll)
  94. const animatedY = coord.y.sub( time.mul( speed ) );
  95. // Create scanline pattern
  96. const scanline = sin( animatedY.mul( count ) );
  97. const scanlineIntensity = scanline.mul( 0.5 ).add( 0.5 ).mul( intensity );
  98. // Darken alternate lines
  99. return color.mul( float( 1.0 ).sub( scanlineIntensity ) );
  100. } );
  101. /**
  102. * Applies vignette effect to darken the edges of the screen.
  103. *
  104. * @tsl
  105. * @function
  106. * @param {Node<vec3>} color - The input color.
  107. * @param {Node<float>} [intensity=0.4] - The intensity of the vignette (0-1).
  108. * @param {Node<float>} [smoothness=0.5] - The smoothness of the vignette falloff.
  109. * @param {Node<vec2>} [coord=uv()] - The UV coordinates to use for vignette calculation.
  110. * @return {Node<vec3>} The color with vignette applied.
  111. */
  112. export const vignette = Fn( ( [ color, intensity = float( 0.4 ), smoothness = float( 0.5 ), coord = uv() ] ) => {
  113. // Use circle for radial gradient (1.42 ≈ √2 covers full diagonal)
  114. const mask = circle( float( 1.42 ), smoothness, coord );
  115. // Apply vignette: center = 1, edges = (1 - intensity)
  116. const vignetteAmount = mix( float( 1.0 ).sub( intensity ), float( 1.0 ), mask );
  117. return color.mul( vignetteAmount );
  118. } );
粤ICP备19079148号