着色器程序与编译
本文档解释 Three.js 如何编译和管理 WebGL 着色器程序,将材质属性和场景光照转换为优化的 GLSL 代码。该系统基于材质特性动态生成着色器,缓存已编译程序以供重用,并管理 WebGL 编译管线。
有关 uniform 如何上传到 GPU 的信息,见更广泛的状态管理文档。有关着色器内执行的光照计算,见光照与阴影。
系统概览
着色器编译系统由两个主要组件组成:WebGLPrograms 管理程序缓存和参数生成,而 WebGLProgram 处理 GLSL 代码的实际编译。该系统为每个材质特性(贴图、光照、效果)组合生成唯一着色器变体,并缓存它们以避免冗余编译。
图表:着色器编译管线
流程从材质需要渲染时开始。WebGLPrograms.getParameters() 分析材质、场景和几何体以产生包含特性标志和定义的参数对象。这生成标识唯一着色器变体的缓存键。如果程序存在于 programsMap 中,则重用;否则,WebGLProgram 通过从 ShaderLib 模板和 ShaderChunk 片段组装 GLSL 来编译新程序。
WebGLPrograms:缓存管理
WebGLPrograms 类维护已编译程序的缓存并生成定义每个唯一着色器变体的参数。它将缓存键映射到 WebGLProgram 实例并跟踪使用计数以进行垃圾回收。
图表:程序获取流
参数生成
getParameters() 函数分析材质属性以确定启用哪些着色器特性。它产生带有布尔标志、数值计数器和字符串标识符的参数对象。
| 参数类型 | 用途 | 示例 |
|---|---|---|
| 材质特性 | 启用/禁用着色器块 | map, normalMap, envMap, clearcoat |
| 光照计数 | 大小 uniform 数组 | numDirLights, numPointLights, numSpotLights |
| 纹理通道 | UV 通道选择 | mapUv, normalMapUv, alphaMapUv |
| 渲染状态 | 输出配置 | toneMapping, shadowMapType, outputColorSpace |
| 几何体特性 | 顶点属性使用 | vertexColors, skinning, morphTargets |
该函数检查 material.map、material.normalMap 等材质属性,并将它们转换为布尔标志(HAS_MAP、HAS_NORMALMAP)。它按类型计数光源,确定使用哪些纹理通道,并检查几何体属性:
// 参数生成逻辑示例(概念性)
const HAS_MAP = !! material.map;
const HAS_NORMALMAP = !! material.normalMap;
const HAS_ANISOTROPY = material.anisotropy > 0;
parameters.map = HAS_MAP;
parameters.normalMap = HAS_NORMALMAP;
parameters.anisotropy = HAS_ANISOTROPY;
parameters.numDirLights = lights.directional.length;缓存键生成
getProgramCacheKey() 函数通过将值连接将参数转换为字符串键。具有相同参数的两个程序产生相同键,实现高效缓存。
键包含:
- 着色器 ID 或自定义着色器哈希值(
shaderID/customVertexShaderID) - 来自
material.defines的所有定义 - 数值参数(精度、光源计数、UV 通道)
- 通过
_programLayers编码为位掩码的布尔标志 - 来自
material.customProgramCacheKey()的自定义缓存键
布尔标志使用 Layers 对象将多个标志打包为整数,减少键长度:
// 标志打包进 _programLayers.mask
_programLayers.enable(0); // instancing
_programLayers.enable(1); // instancingColor
_programLayers.enable(4); // envMap
array.push(_programLayers.mask); // 单个整数程序生命周期
程序通过 usedTimes 引用计数。当材质被渲染时,acquireProgram() 递增计数。当通过 releaseProgram() 释放时,计数递减,在零时删除程序。
WebGLProgram:编译过程
WebGLProgram 构造函数编译顶点和片元着色器并将它们链接为 WebGL 程序。它注入预处理器定义,从 ShaderChunk 解析包含项,并处理 GLSL 版本转换。
图表:着色器编译步骤
前缀注入
构造函数组装包含 GLSL 版本、精度指令、定义和 uniform 声明的着色器前缀。对于非原始材质,广泛的前缀启用/禁用着色器特性:
顶点着色器前缀(摘录):
#version 300 es
precision highp float;
#define SHADER_TYPE MeshStandardMaterial
#define SHADER_NAME MeshStandardMaterial
#define STANDARD
#define USE_MAP
#define USE_NORMALMAP
#define USE_ENVMAP
#define ENVMAP_MODE_REFLECTION
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
// ... 更多 uniform 和定义片元着色器前缀(摘录):
#version 300 es
precision highp float;
#define SHADER_TYPE MeshStandardMaterial
#define STANDARD
#define USE_MAP
#define USE_NORMALMAP
#define TONE_MAPPING
// 内联色调映射函数
vec3 toneMapping( vec3 color ) { return ACESFilmicToneMapping( color ); }
// 内联色彩空间转换
vec4 linearToOutputTexel( vec4 value ) { return sRGBTransferOETF( vec4( value.rgb * mat3(...), value.a ) ); }前缀注入:
- 定义:如
USE_MAP、USE_NORMALMAP、USE_ENVMAP的特性标志 - 精度:所有类型的
highp、mediump或lowp - Uniform:标准矩阵和向量
- 内联函数:色调映射、色彩空间转换、亮度
包含项解析
resolveIncludes() 函数用来自 ShaderChunk 的 GLSL 替换 #include <name> 指令。它递归处理嵌套包含项:
// 模式:#include <chunk_name>
const includePattern = /^[ t]*#include +<([wd./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ];
// 如果未找到块则报错
return resolveIncludes( string ); // 递归
}这允许 ShaderLib 中的着色器模板是模块化的。例如,meshphysical_frag 包含:
#include <common>
#include <packing>
#include <lights_pars_begin>
#include <lights_physical_pars_fragment>
#include <shadowmap_pars_fragment>每个 #include 被替换为来自相应 ShaderChunk 条目的实际 GLSL 代码。
宏替换
包含项解析后,代码用实际值替换占位符宏:
replaceLightNums() 替换光源计数的常量:
// 替换前
#if NUM_DIR_LIGHTS > 0
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
#endif
// 替换后(如果 parameters.numDirLights === 3)
#if 3 > 0
uniform DirectionalLight directionalLights[ 3 ];
#endifreplaceClippingPlaneNums() 替换剪切平面计数:
// NUM_CLIPPING_PLANES → parameters.numClippingPlanes
// UNION_CLIPPING_PLANES → (numClippingPlanes - numClipIntersection)循环展开
unrollLoops() 函数在编译时展开带 pragma 注解的循环以提高 GPU 性能:
// 展开前
#pragma unroll_loop_start
for ( int i = 0; i < 3; i ++ ) {
lights[ i ] = computeLight( i );
}
#pragma unroll_loop_end
// 展开后
lights[ 0 ] = computeLight( 0 );
lights[ 1 ] = computeLight( 1 );
lights[ 2 ] = computeLight( 2 );循环展开消除在动态循环上有困难的 GPU 的分支开销。当光源计数在编译时已知时,系统自动为光源迭代展开循环。
GLSL 3.0 转换
对于非原始材质,系统将 GLSL 1.0 语法转换为 GLSL 3.0 (ES 300):
| GLSL 1.0 | GLSL 3.0 |
|---|---|
| attribute | in(顶点着色器) |
| varying | out(顶点),in(片元) |
| texture2D() | texture() |
| gl_FragColor | layout(location=0) out vec4 pc_fragColor |
此转换允许 ShaderLib 模板使用更简单语法同时输出现代 GLSL。
着色器编译与链接
预处理后,代码创建 WebGL 着色器对象并编译它们:
const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
// 为 morphTargets 强制 'position' 到索引 0
if ( parameters.morphTargets === true ) {
gl.bindAttribLocation( program, 0, 'position' );
}
gl.linkProgram( program );WebGLShader 函数包装 gl.createShader()、gl.shaderSource() 和 gl.compileShader(),返回已编译着色器句柄。
错误处理
编译错误在 onFirstUse() 中检测,该方法在程序首次访问时(对于 uniform 或属性)惰性调用。这通过 KHR_parallel_shader_compile 支持异步编译:
function onFirstUse( self ) {
if ( renderer.debug.checkShaderErrors ) {
const programLog = gl.getProgramInfoLog( program );
const vertexLog = gl.getShaderInfoLog( glVertexShader );
const fragmentLog = gl.getShaderInfoLog( glFragmentShader );
if ( gl.getProgramParameter( program, gl.LINK_STATUS ) === false ) {
const vertexErrors = getShaderErrors( gl, glVertexShader, 'vertex' );
const fragmentErrors = getShaderErrors( gl, glFragmentShader, 'fragment' );
error( 'THREE.WebGLProgram: Shader Error ... ', vertexErrors, fragmentErrors );
}
}
// 提取 uniform 和属性
cachedUniforms = new WebGLUniforms( gl, program );
cachedAttributes = fetchAttributeLocations( gl, program );
}getShaderErrors() 函数解析错误消息以提取行号并显示源的上下文:
function getShaderErrors( gl, shader, type ) {
const errors = gl.getShaderInfoLog( shader ).trim();
const errorMatches = /ERROR: 0:(d+)/.exec( errors );
if ( errorMatches ) {
const errorLine = parseInt( errorMatches[ 1 ] );
return type.toUpperCase() + 'nn' + errors + 'nn' + handleSource( gl.getShaderSource( shader ), errorLine );
}
return errors;
}
function handleSource( string, errorLine ) {
const lines = string.split( 'n' );
const from = Math.max( errorLine - 6, 0 );
const to = Math.min( errorLine + 6, lines.length );
// 返回带行号的格式化源,突出显示错误行
}并行编译
如果 KHR_parallel_shader_compile 扩展可用,程序异步编译。isReady() 方法检查完成:
let programReady = ( parameters.rendererExtensionParallelShaderCompile === false );
this.isReady = function () {
if ( programReady === false ) {
programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );
}
return programReady;
};这允许渲染器启动多个编译而不阻塞,在后续帧中检查完成。
ShaderLib:内置着色器模板
ShaderLib 对象包含标准材质的着色器模板。每个条目定义 uniforms、vertexShader 和 fragmentShader:
图表:ShaderLib 材质着色器
着色器结构
每个着色器条目合并 UniformsLib 组以获得通用功能:
示例:physical 着色器
ShaderLib.physical = {
uniforms: mergeUniforms([
ShaderLib.standard.uniforms,
{
clearcoat: { value: 0 },
clearcoatMap: { value: null },
clearcoatRoughness: { value: 0 },
iridescence: { value: 0 },
sheen: { value: 0 },
transmission: { value: 0 },
thickness: { value: 0 },
// ... 更多物理属性
}
]),
vertexShader: ShaderChunk.meshphysical_vert,
fragmentShader: ShaderChunk.meshphysical_frag
};physical 着色器通过添加清漆、彩虹色、光泽和透射的 uniform 来扩展 standard。顶点和片元着色器引用 ShaderChunk 条目,它们是带 #include 指令的 GLSL 字符串。
Uniform 库
UniformsLib 对象分组相关 uniform:
| 库 | 用途 | Uniforms |
|---|---|---|
| common | 基础材质 | diffuse, opacity, map, alphaMap, alphaTest |
| envmap | 环境映射 | envMap, envMapRotation, reflectivity, ior |
| lights | 光照数组 | directionalLights, pointLights, spotLights, shadow matrices |
| fog | 雾效参数 | fogColor, fogNear, fogFar, fogDensity |
| normalmap | 法线映射 | normalMap, normalScale |
| bumpmap | 凹凸映射 | bumpMap, bumpScale |
| displacementmap | 顶点置换 | displacementMap, displacementScale, displacementBias |
着色器通过 mergeUniforms() 组合这些库:
// Lambert 着色器 uniform
uniforms: mergeUniforms([
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.emissivemap,
UniformsLib.bumpmap,
UniformsLib.normalmap,
UniformsLib.displacementmap,
UniformsLib.fog,
UniformsLib.lights,
{ emissive: { value: new Color(0x000000) }, envMapIntensity: { value: 1 } }
])ShaderChunk:GLSL 片段库
ShaderChunk 对象包含可重用的 GLSL 代码片段。着色器通过 #include <chunk_name> 指令包含这些:
图表:关键 ShaderChunk 片段
通用片段
common 定义数学常数和实用函数:
#define RECIPROCAL_PI 0.3183098861837907
#define EPSILON 1e-6
float pow2( const in float x ) { return x*x; }
vec3 pow2( const in vec3 x ) { return x*x; }
float saturate( const in float a ) { return clamp( a, 0.0, 1.0 ); }bsdfs 提供 BRDF(双向反射分布函数)计算:
float D_BlinnPhong( const in float shininess, const in float dotNH ) {
return RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );
}
vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float shininess ) {
vec3 halfDir = normalize( lightDir + viewDir );
float dotNH = saturate( dot( normal, halfDir ) );
vec3 F = F_Schlick( specularColor, 1.0, dotVH );
float D = D_BlinnPhong( shininess, dotNH );
return F * D * 0.25;
}光照片段
lights_pars_begin 声明光源结构和 uniform:
uniform vec3 ambientLightColor;
struct DirectionalLight {
vec3 direction;
vec3 color;
};
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
struct PointLight {
vec3 position;
vec3 color;
float distance;
float decay;
};
uniform PointLight pointLights[ NUM_POINT_LIGHTS ];lights_physical_pars_fragment 为 MeshStandardMaterial 和 MeshPhysicalMaterial 定义 PBR 函数:
struct PhysicalMaterial {
vec3 diffuseColor;
vec3 specularColor;
float roughness;
float metalness;
#ifdef USE_CLEARCOAT
float clearcoat;
float clearcoatRoughness;
#endif
#ifdef USE_IRIDESCENCE
float iridescence;
float iridescenceIOR;
#endif
};
vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {
// GGX 微表面 BRDF 实现
}lights_fragment_begin 遍历光源并累积贡献:
#pragma unroll_loop_start
for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
directionalLight = directionalLights[ i ];
getDirectionalLightInfo( directionalLight, directLight );
#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
directionalLightShadow = directionalLightShadows[ i ];
directLight.color *= getShadow( directionalShadowMap[ i ], ... );
#endif
RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, material, reflectedLight );
}
#pragma unroll_loop_end阴影片段
shadowmap_pars_fragment 提供阴影采样函数:
float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {
// PCF(百分比接近过滤)实现
// 在 shadowCoord 周围的网格模式中采样阴影图
}
float getPointShadow( samplerCube shadowMap, vec2 shadowMapSize, ... ) {
// 点光源的立方体贴图阴影采样
}shadowmap_vertex 在顶点着色器中计算阴影坐标:
#pragma unroll_loop_start
for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {
shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );
vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;
}
#pragma unroll_loop_end动态着色器生成
着色器基于材质和场景属性定制。系统注入定义、组合片段并生成专门代码路径。
特性定义
参数对象包含成为预处理器定义的布尔标志:
// 在 WebGLPrograms.getParameters() 中
const parameters = {
map: !! material.map,
normalMap: !! material.normalMap,
envMap: !! envMap,
clearcoat: material.clearcoat > 0,
transmission: material.transmission > 0,
shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
// ... 数百个更多
};
// 在 WebGLProgram 构造函数中
prefixFragment = [
'#define SHADER_TYPE ' + parameters.shaderType,
parameters.map ? '#define USE_MAP' : '',
parameters.normalMap ? '#define USE_NORMALMAP' : '',
parameters.envMap ? '#define USE_ENVMAP' : '',
parameters.clearcoat ? '#define USE_CLEARCOAT' : '',
// ...
].filter( filterEmptyLine ).join( 'n' );着色器代码使用这些定义进行条件编译:
#ifdef USE_MAP
vec4 sampledDiffuseColor = texture2D( map, vMapUv );
diffuseColor *= sampledDiffuseColor;
#endif
#ifdef USE_NORMALMAP
vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;
normal = normalize( tbn * mapN );
#endif
#ifdef USE_CLEARCOAT
material.clearcoat = clearcoat;
material.clearcoatRoughness = clearcoatRoughness;
#ifdef USE_CLEARCOATMAP
material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x;
#endif
#endif这在编译时消除非活动代码路径,优化性能。
UV 通道选择
材质可为每个纹理贴图使用不同 UV 通道。系统生成如 MAP_UV、NORMALMAP_UV 的定义:
// 在 getParameters() 中
parameters.mapUv = HAS_MAP && getChannel( material.map.channel );
parameters.normalMapUv = HAS_NORMALMAP && getChannel( material.normalMap.channel );
function getChannel( value ) {
_activeChannels.add( value );
if ( value === 0 ) return 'uv';
return `uv${ value }`;
}这变为:
#define MAP_UV uv
#define NORMALMAP_UV uv1
varying vec2 vMapUv;
varying vec2 vNormalMapUv;
// 在顶点着色器中
vMapUv = ( uvTransform * vec3( uv, 1 ) ).xy;
vNormalMapUv = ( normalMapTransform * vec3( uv1, 1 ) ).xy;光源计数特化
光源数量决定数组大小和循环边界:
parameters.numDirLights = lights.directional.length;
parameters.numPointLights = lights.point.length;
parameters.numSpotLights = lights.spot.length;这些计数替换占位符:
// 替换前
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { ... }
// 替换后(如果 numDirLights = 2)
uniform DirectionalLight directionalLights[ 2 ];
for ( int i = 0; i < 2; i ++ ) { ... }然后循环展开展开循环:
// 展开后
directionalLight = directionalLights[ 0 ];
getDirectionalLightInfo( directionalLight, directLight );
RE_Direct( directLight, ... );
directionalLight = directionalLights[ 1 ];
getDirectionalLightInfo( directionalLight, directLight );
RE_Direct( directLight, ... );色调映射注入
片元着色器前缀基于 parameters.toneMapping 内联注入色调映射函数:
const toneMappingFunctions = {
[ LinearToneMapping ]: 'Linear',
[ ReinhardToneMapping ]: 'Reinhard',
[ CineonToneMapping ]: 'Cineon',
[ ACESFilmicToleMapping ]: 'ACESFilmic',
};
function getToneMappingFunction( functionName, toneMapping ) {
const toneMappingName = toneMappingFunctions[ toneMapping ];
return 'vec3 ' + functionName + '( vec3 color ) { return ' + toneMappingName + 'ToneMapping( color ); }';
}
// 注入到片元前缀中
prefixFragment = [
// ...
( parameters.toneMapping !== NoToneMapping ) ? '#define TONE_MAPPING' : '',
( parameters.toneMapping !== NoToneMapping ) ? getToneMappingFunction( 'toneMapping', parameters.toneMapping ) : '',
// ...
];这生成:
#define TONE_MAPPING
vec3 toneMapping( vec3 color ) { return ACESFilmicToneMapping( color ); }着色器稍后调用 toneMapping( outgoingLight ) 应用色调曲线。
色彩空间转换
与色调映射类似,色彩空间输出函数被注入:
function getTexelEncodingFunction( functionName, colorSpace ) {
const components = getEncodingComponents( colorSpace );
return [
`vec4 ${functionName}( vec4 value ) {`,
` return ${components[1]}( vec4( value.rgb * ${components[0]}, value.a ) );`,
'}'
].join( 'n' );
}
function getEncodingComponents( colorSpace ) {
ColorManagement._getMatrix( _m0, ColorManagement.workingColorSpace, colorSpace );
const encodingMatrix = `mat3( ${ _m0.elements.map( v => v.toFixed(4) ) } )`;
switch ( ColorManagement.getTransfer( colorSpace ) ) {
case LinearTransfer:
return [ encodingMatrix, 'LinearTransferOETF' ];
case SRGBTransfer:
return [ encodingMatrix, 'sRGBTransferOETF' ];
}
}这生成矩阵乘法和传递函数:
vec4 linearToOutputTexel( vec4 value ) {
return sRGBTransferOETF( vec4( value.rgb * mat3( 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ), value.a ) );
}着色器调用此函数从线性转换为输出色彩空间(sRGB、Display P3 等)。
Uniform 管理
程序链接后,系统通过 WebGLUniforms 提取 uniform 位置并创建缓存结构。这在 onFirstUse() 中惰性发生。
Uniform 位置提取
WebGLUniforms 构造函数查询 WebGL 程序的所有活动 uniform:
cachedUniforms = new WebGLUniforms( gl, program );
// 在 WebGLUniforms 内
const n = gl.getProgramParameter( program, gl.ACTIVE_UNIFORMS );
for ( let i = 0; i < n; i++ ) {
const info = gl.getActiveUniform( program, i );
const name = info.name;
const addr = gl.getUniformLocation( program, name );
// 存储位置和类型
}这创建将名称映射到 WebGL 位置的 uniform 缓存,无需重复 getUniformLocation() 调用即可高效更新。
属性位置提取
类似地,fetchAttributeLocations() 查询顶点属性:
function fetchAttributeLocations( gl, program ) {
const attributes = {};
const n = gl.getProgramParameter( program, gl.ACTIVE_ATTRIBUTES );
for ( let i = 0; i < n; i++ ) {
const info = gl.getActiveAttrib( program, i );
const name = info.name;
let locationSize = 1;
if ( info.type === gl.FLOAT_MAT2 ) locationSize = 2;
if ( info.type === gl.FLOAT_MAT3 ) locationSize = 3;
if ( info.type === gl.FLOAT_MAT4 ) locationSize = 4;
attributes[ name ] = {
type: info.type,
location: gl.getAttribLocation( program, name ),
locationSize: locationSize
};
}
return attributes;
}矩阵占用多个属性位置(例如,mat4 使用 4 个连续位置),因此 locationSize 跟踪此信息。
惰性初始化
Uniform 和属性提取延迟到首次使用以支持异步编译:
this.getUniforms = function () {
if ( cachedUniforms === undefined ) {
onFirstUse( this ); // 填充 cachedUniforms
}
return cachedUniforms;
};
this.getAttributes = function () {
if ( cachedAttributes === undefined ) {
onFirstUse( this ); // 填充 cachedAttributes
}
return cachedAttributes;
};这允许渲染器启动编译,继续其他工作,仅在程序就绪时访问 uniform。
程序使用示例
完整着色器编译流:
图表:完整程序获取流
过程从渲染器需要绘制材质时开始。它生成描述材质特性的参数,创建缓存键,并检查是否存在匹配程序。缓存未命中时,WebGLProgram 从着色器模板编译新程序,插入定义和处理包含项。程序为具有 identical 参数的未来绘制缓存。Uniform 和属性在首次访问时惰性提取,支持异步编译。