
Manim 着色器代码共享机制深入解析 GLSL 的 #include/#INSERT 内联替换原理【免费下载链接】manimA community-maintained Python framework for creating mathematical animations.项目地址: https://gitcode.com/GitHub_Trending/man/manim导读本文讲解 Manim开源数学动画框架OpenGL 渲染器中一个容易被忽略但至关重要的底层机制着色器Shader之间的代码共享与内联插入。OpenGL 原生 GLSL 并不像 C/C 那样提供#include预处理指令Manim 通过一套自建的文本替换方案让顶点着色器、几何着色器与片元着色器能够复用公共代码段相机 uniform 声明、坐标变换、贝塞尔距离场、光照计算等。读完本文你将理解manim/renderer/shaders/include/NOTE.md所描述的#INSERT设计思想、当前仓库中实际的#include ../include/实现语法、内联展开的完整执行链路以及如何利用这套机制为 Mobject 注入自定义着色逻辑。一、问题背景GLSL 为什么需要一套自建的includeManim 的 OpenGL 渲染器依赖 moderngl 驱动着色器程序见 shader_wrapper.py渲染一座数学对象通常需要顶点着色器vert、几何着色器geom与片元着色器frag协同工作。在渲染一个场景时多个不同的 shader 之间往往存在大量重复代码——例如每个着色器几乎都要用到相机相关的 uniformframe_shape、camera_center、focal_distance等以及把物体坐标变换进相机帧、计算最终颜色这类通用函数。正如 include/NOTE.md 开门见山指出的There seems to be no analog to #include in C for OpenGL shaders.即 OpenGL 着色语言没有 C/C 意义上的#include宏预处理。虽然业界存在其他共享着色器代码的方案例如运行时拼接字符串、使用第三方预处理器、通过 uniform 传参等但很多方案并不理想尤其是当目标是由着色器文件自身决定要共享哪些代码片段时外部工具往往难以满足这种声明式诉求。Manim 的做法是在 GLSL 源文件中直接写入一行占位指令再由渲染器在编译前用include目录下的真实代码逐字替换该行。NOTE.md 中描述的语法是#INSERT file_name而当前仓库get_shader_code_from_file的实现实际识别的是#include ../include/xxx.glsl形式见下文第三节两种语法本质都是整行替换的内联模型这也是理解整套机制的核心。二、include 目录着色器代码片段库所有可被共享的代码片段都集中在 manim/renderer/shaders/include/ 目录下当前包含以下.glsl文件文件作用camera_uniform_declarations.glsl声明相机相关 uniform 变量帧形状、抗锯齿宽度、相机中心、旋转矩阵、透视焦距等get_gl_Position.glsl提供get_gl_Position把三维点变换到裁剪坐标含透视缩放与固定于帧内逻辑position_point_into_frame.glsl提供position_point_into_frame/rotate_point_into_frame实现相机旋转与平移finalize_color.glsl提供finalize_color/add_light/float_to_color负责颜色与光照计算quadratic_bezier_distance.glsl二次贝塞尔曲线的有向距离场SDF求解quadratic_bezier_geometry_functions.glsl二次贝塞尔几何辅助函数get_unit_normal.glsl由三个顶点计算单位法向量含退化情形的兜底get_rotated_surface_unit_normal_vector.glsl曲面片旋转后的单位法向量add_light.glsl光照叠加辅助函数NOTE.md 特别强调了一个重要约束这些片段函数往往引用了外部的 uniform 变量而这些 uniform 被假定存在于它们被插入的上下文之中。也就是说include 片段并不是自包含的模块它们依赖宿主着色器中已经声明好的 uniform 与函数。例如get_gl_Position.glsl开头就以注释形式列出了其前置依赖// Assumes the following uniforms exist in the surrounding context: // uniform vec2 frame_shape; // uniform float focal_distance; // uniform float is_fixed_in_frame; // uniform float is_fixed_orientation; // uniform vec3 fixed_orientation_center;这些 uniform 恰好由camera_uniform_declarations.glsl声明因此宿主着色器的标准做法是先 include 声明再 include 使用它们的函数。三、实际调用链从#include到内联展开3.1 宿主着色器中的用法当前仓库中所有着色器统一使用#include ../include/xxx.glsl形式。以二次贝塞尔填充的片元着色器 quadratic_bezier_fill/frag.glsl 为例#version 330 #include ../include/camera_uniform_declarations.glsl ... // Needed for quadratic_bezier_distance insertion below float modify_distance_for_endpoints(vec2 p, float dist, float t){ return dist; } #include ../include/quadratic_bezier_distance.glsl这里体现了 NOTE.md 提到的上下文依赖quadratic_bezier_distance.glsl内部调用了modify_distance_for_endpoints见 quadratic_bezier_distance.glsl 中dist_to_line、dist_to_point_on_curve等函数因此宿主文件必须在 include 该片段之前先定义这个函数。换言之include 片段与宿主之间存在契约顺序错了会导致 GLSL 编译失败。类似的用法遍布渲染管线simple_vert.glsl基础顶点着色器依次 include 相机声明、get_gl_Position、position_point_into_framequadratic_bezier_fill/geom.glsl几何着色器include 贝塞尔几何函数、get_gl_Position、get_unit_normal、finalize_colorsurface/vert.glsl曲面额外 includeget_rotated_surface_unit_normal_vectorimage/vert.glsl、true_dot/vert.glsl 等亦采用同一模式。3.2 内联替换的实现源码真正的预处理器实现位于 shader_wrapper.py 的get_shader_code_from_file函数def get_shader_code_from_file(filename: Path) - str | None: if filename in filename_to_code_map: return filename_to_code_map[filename] try: filepath find_file( filename, directories[get_shader_dir(), Path(/)], ) except OSError: return None result filepath.read_text() # To share functionality between shaders, some functions are read in # from other files an inserted into the relevant strings before # passing to ctx.program for compiling # Replace #INSERT lines with relevant code insertions re.findall( r^#include ../include/.*\.glsl$, result, flagsre.MULTILINE, ) for line in insertions: inserted_code get_shader_code_from_file( Path() / include / line.replace(#include ../include/, ), ) if inserted_code is None: return None result result.replace(line, inserted_code) filename_to_code_map[filename] result return result这套实现包含三个关键设计递归展开get_shader_code_from_file处理到#include行时会先递归读取目标片段文件而片段文件本身也可以再包含其他片段形成多级嵌套。同时注释中明确保留了对 NOTE.md 中#INSERT语法的历史说明# Replace #INSERT lines with relevant code。结果缓存展开后的完整 GLSL 源码会被缓存在模块级字典filename_to_code_map见 shader_wrapper.py中避免同一 shader 被重复解析这正是性能敏感的视频渲染所必需的。编译前注入展开后的代码最终由ShaderWrapper见 shader_wrapper.py作为program_code交给 moderngl 的ctx.program编译而ShaderWrapper同时维护 uniform 字典、纹理路径、深度测试开关与渲染图元类型从而把代码展开与程序状态统一管理。3.3 为什么用整行文本替换而非真正预处理从实现上看这套机制本质上是一个受限的、按行匹配的文本内联器与 C 预处理器的宏展开有本质区别它不做宏定义、不做条件编译、不做令牌级处理仅识别形如#include ../include/名称.glsl的整行并替换为文件内容。好处在于——正如 NOTE.md 所言——共享哪些代码的逻辑完全由着色器文件自身声明无需额外的构建步骤或第三方工具GLSL 源文件就是唯一事实来源代价则是片段与宿主之间的依赖关系只能靠注释约定与 include 顺序来保证。四、典型片段剖析相机 uniform 与坐标变换4.1 相机 uniform 声明camera_uniform_declarations.glsl 是所有顶点着色器的公共头文件uniform vec2 frame_shape; uniform float anti_alias_width; uniform vec3 camera_center; uniform mat3 camera_rotation; uniform float is_fixed_in_frame; uniform float is_fixed_orientation; uniform vec3 fixed_orientation_center; uniform float focal_distance;这些 uniform 由渲染器在运行时填充对应相机状态、帧尺寸与焦距而它们的分发入口同样在 shader_wrapper.py 的ShaderWrapper.uniforms中。所有顶点着色器只需 include 这一行声明即可保证相机状态在整套着色器程序中保持一致——这正是该片段被 simple_vert.glsl、quadratic_bezier_fill/vert.glsl、quadratic_bezier_stroke/vert.glsl、surface/vert.glsl、true_dot/vert.glsl 等十余处反复引用的原因。4.2 帧内定位与透视get_gl_Position.glsl 展示了这套共享机制如何承载核心数学float perspective_scale_factor(float z, float focal_distance){ return max(0.0, focal_distance / (focal_distance - z)); } vec4 get_gl_Position(vec3 point){ vec4 result vec4(point, 1.0); if(!bool(is_fixed_in_frame)){ result.x * 2.0 / frame_shape.x; result.y * 2.0 / frame_shape.y; float psf perspective_scale_factor(result.z, focal_distance); if (psf 0){ result.xy * psf; result.z * 0.01; } } else { // 固定于帧内的物体按默认帧比例或帧形状缩放 } result.z * -1; return result; }该函数同时处理了两种模式普通物体的透视投影缩放以及is_fixed_in_frame时钉在帧内如固定 UI 元素的平铺逻辑是 Manim 固定于帧内fixed-in-frame特性的 GLSL 根基。4.3 相机旋转与平移position_point_into_frame.glsl 则负责把世界坐标旋转/平移到相机坐标系vec3 rotate_point_into_frame(vec3 point){ if(bool(is_fixed_in_frame)){ return point; } return camera_rotation * point; } vec3 position_point_into_frame(vec3 point){ if(bool(is_fixed_in_frame)){ return point; } if(bool(is_fixed_orientation)){ vec3 new_center rotate_point_into_frame(fixed_orientation_center); return point (new_center - fixed_orientation_center); } return rotate_point_into_frame(point - camera_center); }顶点着色器的典型组合拳是position_point_into_frame先做相机变换再交给get_gl_Position做透视与裁剪归一化最终得到gl_Position。这一两段式管线被所有顶点着色器复用避免了在十余个 vert 文件中重复编写同一套相机数学。五、进阶贝塞尔距离场与颜色注入5.1 二次贝塞尔 SDF 的模块化quadratic_bezier_distance.glsl 是渲染管线中最复杂的共享片段它实现二次贝塞尔曲线的有向距离场包含三次方程求根cubic_solve、点到曲线最小距离min_dist_to_curve等约一百行 GLSL。该片段同时被填充着色器 quadratic_bezier_fill/frag.glsl 与描边着色器 quadratic_bezier_stroke/frag.glsl 引用让曲线边缘抗锯齿AA这一核心视觉质量特性只需维护一份实现。而get_unit_normal.glsl提供的法向量计算含三点共线等退化情形的兜底则被几何着色器用于光照与描边扩张。5.2 可注入的着色器扩展点set_color_by_codefinalize_color.glsl 是这套机制最具可扩展性的体现vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss, float shadow){ ///// INSERT COLOR FUNCTION HERE ///// // The line above may be replaced by arbitrary code snippets, as per // the method Mobject.set_color_by_code return add_light(color, point, unit_normal, light_coords, gloss, shadow); }片段源码中的///// INSERT COLOR FUNCTION HERE /////注释是一个预留扩展点Manim 允许通过Mobject.set_color_by_code把用户自定义的 GLSL 代码片段注入到这个位置从而实现任意颜色函数。这正呼应了 NOTE.md 提出的设计目标——由着色器文件自身声明要共享/替换哪些代码把扩展点以注释形式固化在共享片段里再由上层 Python API 做精准替换构成一条从 Python 到 GLSL 的自定义着色链路。六、总结这套机制的设计取舍与扩展价值回顾整个实现可以提炼出 Manim 着色器共享机制的三个设计要点零依赖的文本内联不依赖任何第三方 GLSL 预处理器仅用正则匹配#include ../include/xxx.glsl整行并递归展开展开结果按文件缓存shader_wrapper.py实现简单、行为可预期、便于调试。声明式共享共享什么、共享谁的决策权在 GLSL 文件本身宿主文件写 include片段文件通过注释声明前置依赖与 NOTE.md 的设计初衷完全一致。上下文契约 扩展点片段函数引用外部 uniform 与函数如modify_distance_for_endpoints依赖 include 顺序与注释约定同时通过finalize_color中的注入点与Mobject.set_color_by_code对接为深度用户保留自定义着色能力。对于想深入 Manim OpenGL 渲染管线manim/renderer/opengl_renderer.py或自行编写定制 shader 的开发者建议按以下顺序阅读代码先浏览 shaders/include/ 目录建立公共片段库的全局观再对照 shader_wrapper.py 理解内联展开与缓存最后挑一个典型着色器如 quadratic_bezier_fill/ 的 vert/geom/frag 三件套完整追踪一次include → 展开 → 编译 → 渲染的流程。掌握了这套内联机制你就掌握了 Manim OpenGL 渲染器着色系统的一块关键拼图。【免费下载链接】manimA community-maintained Python framework for creating mathematical animations.项目地址: https://gitcode.com/GitHub_Trending/man/manim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考