Babylon.js的Shader入门一(从只有一个颜色的Shader说起)
一、顶点着色器
文件名BaseColor.vertex.fx
attribute vec3 position;
uniform mat4 worldViewProjection;
void main() {
gl_Position = worldViewProjection * vec4(position, 1.0);
}
1、关于attribute
和 uniform
在 GLSL 着色器和 Babylon.js 的实现中,attribute
和 uniform
是两种完全不同的变量类型,它们的区别体现在以下核心维度:
1)本质差异对比表
特性 | attribute | uniform |
---|---|---|
作用阶段 | 仅在顶点着色器可用 | 顶点/片段着色器均可使用 |
数据来源 | 从顶点缓冲区(VBO)读取 | 由 CPU 端设置(开发者或引擎) |
更新频率 | 每个顶点都会变化 | 同一绘制调用中所有顶点/片元相同 |
典型用途 | 顶点位置、法线、UV坐标等 | 变换矩阵、颜色参数、时间值等 |
Babylon.js 传递方式 | 需在 attributes 数组中声明 | 需在 uniforms 数组中声明 |
2)数据流示意图
[CPU 端]
│
├── uniform 数据(如矩阵、颜色) → 通过 setXXX() 方法传入 → [Shader]
│
└── 顶点缓冲区(VBO) → attribute 数据 → [顶点着色器]
3)在示例代码中的具体表现
(1) attribute vec3 position
-
作用:接收来自网格顶点缓冲区的坐标数据
-
数据来源:
const box = MeshBuilder.CreateBox(...); // 自动生成顶点坐标
-
更新规则:每个顶点的位置值不同(如立方体有8个顶点,每个顶点都有独立的position值)
(2) uniform mat4 worldViewProjection
-
作用:传递组合变换矩阵
-
数据来源:
// Babylon.js 每帧自动计算: const matrix = mesh.getWorldMatrix() * camera.getViewMatrix() * camera.getProjectionMatrix();
-
更新规则:同一物体的所有顶点共享同一个矩阵值
(3) uniform vec3 diffuseColor
-
作用:自定义颜色参数
-
数据设置:
myMaterial.setColor3("diffuseColor", ...); // 手动设置
-
更新规则:整个物体使用同一颜色值
(4)Babylon.js 的特殊处理机制
-
attribute 自动绑定:
当在 attributes
数组中声明 "position"
时,Babylon.js 会自动关联到网格的顶点位置缓冲区,其他常用自动绑定的 attribute:normal
:顶点法线、uv
:纹理坐标、color
:顶点颜色
- uniform 更新策略:
uniform 类型 | 更新频率 | 典型示例 |
---|---|---|
引擎管理 | 每帧 | worldViewProjection |
开发者控制 | 按需 | diffuseColor |
- Babylon.js 还支持自动传递这些标准矩阵:
Uniform 名称 | 对应矩阵 | 典型用途 |
---|---|---|
world | 世界矩阵 | 物体自身变换 |
view | 视图矩阵 | 摄像机视角 |
projection | 投影矩阵 | 透视/正交效果 |
worldView | 世界视图矩阵 | 组合变换计算 |
2、关于gl_Position
1)gl_Position
的真实含义
gl_Position
存储的是顶点在**裁剪空间(Clip Space)**中的坐标,并非直接的屏幕位置。这是图形渲染管线中最重要的中间坐标阶段。
2)坐标系转换全流程(逐步分析)
模型空间(Model Space)
- 原始顶点坐标(示例中的
position
属性) - 示例值:
(0,0,0)
到(1,1,1)
(立方体顶点)
世界空间(World Space)
- 通过
worldMatrix
变换 - 物体在场景中的实际位置和姿态
vec4 worldPos = worldMatrix * vec4(position, 1.0);
观察空间(View Space)
- 通过
viewMatrix
变换 - 以摄像机为原点的坐标系
vec4 viewPos = viewMatrix * worldPos;
裁剪空间(Clip Space)
- 通过
projectionMatrix
变换 - 坐标范围:
[-w, w]
(w 是齐次坐标的w分量)
gl_Position = projectionMatrix * viewPos; // 等价于 worldViewProjection * modelPos
标准化设备坐标(NDC)
- 通过透视除法自动转换
- 范围:
[-1, 1]^3
vec3 ndc = gl_Position.xyz / gl_Position.w;
视口变换(Viewport Transform)
- 最终映射到屏幕像素
- 由
gl.viewport
设置决定
vec2 screenPos = (ndc.xy * 0.5 + 0.5) * viewportSize;
3)关键特性对比表
特性 | 裁剪空间坐标 (gl_Position ) | 屏幕坐标 |
---|---|---|
坐标系类型 | 齐次坐标系 | 笛卡尔坐标系 |
坐标范围 | 任意(需满足 -w ≤ x,y,z ≤ w) | [0, width] x [0, height] |
自动转换阶段 | 由硬件完成透视除法和视口变换 | 最终输出结果 |
开发者控制权 | 完全控制 | 间接受 viewport 设置影响 |
4)通过实验验证坐标变换
(1)修改顶点着色器输出:
// 强制所有顶点位于裁剪空间中心
gl_Position = vec4(0, 0, 0, 1);
效果:所有顶点将集中在屏幕中心点(NDC的(0,0)映射到屏幕中心)
(2) 超出裁剪空间的顶点:
gl_Position = vec4(2.0, 2.0, 0.0, 1.0); // x/w=2.0 > 1.0
效果:该顶点会被裁剪(不生成片元),因为不满足 -1 ≤ x/w ≤ 1
5)Babylon.js 的矩阵自动化
当使用 worldViewProjection
矩阵时,Babylon.js 已经帮你完成了:
\begin{aligned}
\text{worldViewProjection} &= \text{World} \times \text{View} \times \text{Projection} \\
&= \begin{bmatrix}
\cdots & \text{组合变换} & \cdots \\
\end{bmatrix}
\end{aligned}
这使得开发者无需手动处理各阶段的矩阵乘法。
6)典型错误认知纠正
❌ 误区:"gl_Position 直接对应屏幕像素坐标"
正确理解:
-
gl_Position
需要经过:
- 透视除法:
(x/w, y/w, z/w)
- 视口映射:将 NDC 的 [-1,1]² 映射到屏幕区域
❌ 误区:"w 分量可以忽略"
关键作用:
-
w 分量控制透视效果(实现近大远小)
-
深度值(z/w) 影响深度测试
-
齐次坐标的归一化
7)实际开发建议
(1)调试裁剪空间坐标:
// 在片段着色器中可视化裁剪空间坐标
gl_FragColor = vec4(gl_FragCoord.xyz / gl_FragCoord.w, 1.0);
(2)处理超大场景:
- 当物体超出远裁剪面(通常 z/w > 1),会自动被裁剪
- 需合理设置投影矩阵参数:
camera.minZ = 0.1; // 近裁剪面
camera.maxZ = 100.0; // 远裁剪面
(3)高级应用示例(变形动画):
// 在裁剪空间添加正弦波动画
float wave = sin(time + position.x * 5.0) * 0.2;
gl_Position = worldViewProjection * vec4(position.x, position.y + wave, position.z, 1.0);
二、片元着色器
文件BaseColor.fragment.fx
precision mediump float;
uniform vec3 diffuseColor;
void main() {
gl_FragColor = vec4(diffuseColor, 1.0);
}
1、精度声明
precision mediump float;这一行
是 GLSL(OpenGL Shading Language)中的精度修饰符声明,在 WebGL 着色器中有以下核心作用:
1)定义浮点数精度:
highp
表示高精度浮点数(通常是32位)mediump
表示中精度(通常是16位)lowp
表示低精度(通常≤10位)
2)在WebGL环境中的必要性:
- 顶点着色器中默认有
highp
精度 - 片段着色器必须显式声明精度(否则可能报错)
- 在移动设备中建议使用mediump
3)健壮的写法:
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
2、关于gl_FragColor
gl_FragColor
是片段着色器的输出颜色,但严格来说它还不是屏幕上的最终颜色。这是图形渲染中一个关键但需要细致理解的概念,让我们通过完整的渲染流程来解析它的真实角色:
1)gl_FragColor
的定位与转换流程
以下是完整的颜色处理流程:
片段着色器计算
→ [gl_FragColor]
→ 模板测试(Stencil Test)
→ 深度测试(Depth Test)
→ 混合(Blending)
→ 颜色缓冲(Color Buffer)
→ 最终屏幕像素
2)各阶段对颜色的影响详解
(1)片段着色器输出阶段
gl_FragColor = vec4(diffuseColor, 1.0); // 输出 RGBA 值
- 角色:定义当前片段的"候选颜色"
- 存储位置:暂存于片段处理单元
- 此时的状态:可能被后续流程修改或丢弃
(2)模板测试(Stencil Test)
- 作用:通过模板缓冲区值决定是否保留片段
- 示例场景:制作UI遮罩、镜面反射区域
- 影响结果:若未通过测试,直接丢弃该片段
(3)深度测试(Depth Test)
- 判断条件:
if(current_fragment.depth > depth_buffer_value) discard;
- 典型表现:遮挡关系处理(近处物体覆盖远处物体)
- Babylon.js 控制方式:
material.depthFunction = BABYLON.Engine.NEVER; // 自定义测试规则
(4)混合(Blending)
- 触发条件:当材质开启 alpha 混合时
- 计算公式:
最终颜色 = srcColor * srcAlpha + dstColor * (1 - srcAlpha)
Babylon.js 设置
material.alphaMode = BABYLON.Engine.ALPHA_COMBINE; // 启用混合
material.separateCullingPass = true; // 优化半透明渲染
(5)颜色缓冲写入
- 最终存储:通过所有测试的颜色值写入帧缓冲区
- 后期处理影响:可能经过抗锯齿、HDR 色调映射等处理
3)关键特性对比表
特性 | gl_FragColor | 屏幕最终颜色 |
---|---|---|
决定阶段 | 片段着色器输出 | 所有测试/混合后的结果 |
修改可能性 | 可能被后续测试丢弃 | 实际可见的稳定值 |
数据存储 | 临时片段数据 | 帧缓冲区(最终像素) |
开发者控制程度 | 完全控制 | 间接影响 |
4)通过实验验证流程
实验1:强制修改深度值
// 在片段着色器中:
gl_FragColor = vec4(1,0,0,1);
gl_FragDepth = 0.0; // 强制深度值为最近
效果:该片段会覆盖所有后续渲染的更深层片段
实验2:半透明混合效果
// 在材质设置中:
material.alpha = 0.5; // 设置半透明
material.backFaceCulling = false; // 显示双面
效果:物体呈现半透明状态,能看到后方景物
5)现代 WebGL 的扩展用法(WebGL2+)
在更现代的实践中:
// 使用显式输出定位(替代 gl_FragColor)
out vec4 finalColor;
void main() {
finalColor = vec4(diffuseColor, 1.0);
}
-
优势:支持多渲染目标(MRT)
-
兼容性:需要 WebGL2 上下文支持
6)Babylon.js 的封装逻辑
引擎在底层自动处理:
-
帧缓冲区管理:自动创建/绑定颜色缓冲
-
测试状态控制:默认开启深度测试和背面剔除
- 混合模式预设:提供多种混合模式选项
material.alphaMode = BABYLON.Engine.ALPHA_ADD; // 叠加模式
7)实际开发注意事项
(1)性能优化:
- 尽早通过
discard;
丢弃不需要的片段
if(alpha < 0.01) discard; // 减少后续计算
(2)颜色空间:
- WebGL 默认使用线性颜色空间
- 需要手动做 Gamma 校正:
vec3 gammaCorrected = pow(color, vec3(1.0/2.2));
gl_FragColor = vec4(gammaCorrected, 1.0);
(3)HDR 处理
// 使用浮点帧缓冲区时
gl_FragColor = vec4(color * exposure, 1.0);
总之gl_FragColor
不是屏幕最终颜色,而是:
-
片段着色器输出的"候选颜色"
-
需要经过深度测试、模板测试、混合等后续处理
-
最终可能被修改、混合或完全丢弃
在示例代码中,由于:
-
没有开启混合(alpha=1.0)
-
默认通过深度测试
-
没有后期处理效果
因此 gl_FragColor
会直接写入颜色缓冲,但严格来说它仍然要经过渲染管线的完整处理流程才能成为最终像素颜色。
三、Babylon.js使用示例
// 创建自定义着色器材质
const myMaterial = new BABYLON.ShaderMaterial("myShader", scene,
"./src/Shader/BaseColor",
{
attributes: ["position"],
uniforms: ["worldViewProjection", "diffuseColor"]
});
// 设置默认颜色(可以后续通过material.setColor3修改)
myMaterial.setColor3("diffuseColor", BABYLON.Color3.Red());
// 应用到网格
const box = BABYLON.MeshBuilder.CreateBox("box", {size: 2}, scene);
box.material = myMaterial;
这里需要说明的是,BABYLON.ShaderMaterial的第三个参数"./src/Shader/BaseColor"描述了shader文件的保存位置("./src/Shader")和总体名称("BaseColor"),Babylon可据此找到"BaseColor.vertex.fx"文件和 "BaseColor.fragment.fx"文件。