shaders.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. from __future__ import annotations
  2. import os
  3. import re
  4. from functools import lru_cache
  5. import moderngl
  6. from PIL import Image
  7. import numpy as np
  8. from manimlib.utils.directories import get_shader_dir
  9. from manimlib.utils.file_ops import find_file
  10. from typing import TYPE_CHECKING
  11. if TYPE_CHECKING:
  12. from typing import Sequence, Optional, Tuple
  13. from manimlib.typing import UniformDict
  14. from moderngl.vertex_array import VertexArray
  15. from moderngl.framebuffer import Framebuffer
  16. # Global maps to reflect uniform status
  17. PROGRAM_UNIFORM_MIRRORS: dict[int, dict[str, float | tuple]] = dict()
  18. @lru_cache()
  19. def image_path_to_texture(path: str, ctx: moderngl.Context) -> moderngl.Texture:
  20. im = Image.open(path).convert("RGBA")
  21. return ctx.texture(
  22. size=im.size,
  23. components=len(im.getbands()),
  24. data=im.tobytes(),
  25. )
  26. @lru_cache()
  27. def get_shader_program(
  28. ctx: moderngl.context.Context,
  29. vertex_shader: str,
  30. fragment_shader: Optional[str] = None,
  31. geometry_shader: Optional[str] = None,
  32. ) -> moderngl.Program:
  33. return ctx.program(
  34. vertex_shader=vertex_shader,
  35. fragment_shader=fragment_shader,
  36. geometry_shader=geometry_shader,
  37. )
  38. def set_program_uniform(
  39. program: moderngl.Program,
  40. name: str,
  41. value: float | tuple | np.ndarray
  42. ) -> bool:
  43. """
  44. Sets a program uniform, and also keeps track of a dictionary
  45. of previously set uniforms for that program so that it
  46. doesn't needlessly reset it, requiring an exchange with gpu
  47. memory, if it sees the same value again.
  48. Returns True if changed the program, False if it left it as is.
  49. """
  50. pid = id(program)
  51. if pid not in PROGRAM_UNIFORM_MIRRORS:
  52. PROGRAM_UNIFORM_MIRRORS[pid] = dict()
  53. uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid]
  54. if type(value) is np.ndarray and value.ndim > 0:
  55. value = tuple(value.flatten())
  56. if uniform_mirror.get(name, None) == value:
  57. return False
  58. try:
  59. program[name].value = value
  60. except KeyError:
  61. return False
  62. uniform_mirror[name] = value
  63. return True
  64. @lru_cache()
  65. def get_shader_code_from_file(filename: str) -> str | None:
  66. if not filename:
  67. return None
  68. try:
  69. filepath = find_file(
  70. filename,
  71. directories=[get_shader_dir(), "/"],
  72. extensions=[],
  73. )
  74. except IOError:
  75. return None
  76. with open(filepath, "r") as f:
  77. result = f.read()
  78. # To share functionality between shaders, some functions are read in
  79. # from other files an inserted into the relevant strings before
  80. # passing to ctx.program for compiling
  81. # Replace "#INSERT " lines with relevant code
  82. insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE)
  83. for line in insertions:
  84. inserted_code = get_shader_code_from_file(
  85. os.path.join("inserts", line.replace("#INSERT ", ""))
  86. )
  87. result = result.replace(line, inserted_code)
  88. return result
  89. def get_colormap_code(rgb_list: Sequence[float]) -> str:
  90. data = ",".join(
  91. "vec3({}, {}, {})".format(*rgb)
  92. for rgb in rgb_list
  93. )
  94. return f"vec3[{len(rgb_list)}]({data})"