CesiumJS 源码杂谈 - 从光到 Uniform
之前对实时渲染(RealTimeRendering)的殿堂就十分向往,也有简单了解过实时渲染中的光,无奈一直没能系统学习。鉴于笔者已经有一点 CesiumJS 源码基础,所以就抽了一个周末跟了跟 CesiumJS 中的光照初步,在简单的代码追踪后,发现想系统学习光照材质,仍然是需要 RTR 知识的,这次仅仅了解了光在 CesiumJS 底层中是如何从 API 传递到 WebGL 着色器中去的,为之后深入研究打下基础。
1. 有什么光
CesiumJS 支持的光的类型比较少,默认场景光就一个太阳光:
// Scene 类构造函数中
this.light = new SunLight();
从上面这代码可知,CesiumJS 目前场景中只支持加入一个光源。
查阅 API,可得知除了 SubLight 之外,还有一个 DirectionalLight,即方向光。
官方示例代码《Lighting》中就使用了方向光来模拟手电筒效果(flashLight)、月光效果(moonLight)、自定义光效果。
方向光比太阳光多出来一个必选的方向属性:
const flashLight = new DirectionalLight({
direction: scene.camera.directionWC // 每帧都不一样,手电筒一直沿着相机视线照射
})
这个 direction 属性是一个单位向量即可(模长是 1)。
说起来归一化、规范化、标准化好像都能在网上找到与单位向量类似的意思,都是向量除以模长。
可见,CesiumJS 并没有内置点光源、聚光灯,需要自己写着色过程(请参考 Primitive API 或 CustomShader API)。
2. 光如何转换成 Uniform 以及何时被调用
既然 CesiumJS 支持的光只有一个,那么调查起来就简单了。先给结论:
光是作为 Uniform 值传递到着色器中的。 先查清楚光是如何从 Scene.light 转至 Renderer 中的 uniform 的。
2.1. 统一值状态对象(UniformState)
在 Scene 渲染一帧的过程中,几乎就在最顶部,Scene.js 模块内的函数 render 就每帧更新着 Context 对象的 uniformState 属性:
function render(scene) {
const frameState = scene._frameState;
const context = scene.context;
const us = context.uniformState;
// ...
us.update(frameState);
// ...
}
这个 uniformState 对象就是 CesiumJS 绝大多数统一值(Uniform)的封装集合,它的更新方法就会更新来自帧状态对象(FrameState)的光参数:
UniformState.prototype.update = function (frameState) {
// ...
const light = defaultValue(frameState.light, defaultLight);
if (light instanceof SunLight) { /**/ }
else { /**/ }
const lightColor = light.color;
// 计算 HDR 光到 this._lightColor 上
// ...
}
那么,这个挂在 Context 上的 uniformState 对象包含的光状态信息,是什么时候被使用的呢?下一小节 2.2 就会介绍。
2.2. 上下文(Context)执行 DrawCommand
在 Scene 的更新过程中,最后 DrawCommand 对象被 Context 对象执行:
function continueDraw(context, drawCommand, shaderProgram, uniformMap) {
// ...
shaderProgram._setUniforms(
uniformMap,
context._us,
context.validateShaderProgram
)
// ...
}
Context.prototype.draw = function (/* ... */) {
// ...
shaderProgram = defaultValue(shaderProgram, drawCommand._shaderProgram);
uniformMap = defaultValue(uniformMap, drawCommand._uniformMap);
beginDraw(this, framebuffer, passState, shaderProgram, renderState);
continueDraw(this, drawCommand, shaderProgram, uniformMap);
}
就在 continueDraw 函数中,调用了 ShaderProgram 对象的 _setUniforms 方法,所有 Uniform 值在此将传入 WebGL 状态机中。
ShaderProgram.prototype._setUniforms = function (/**/) {
// ...
const uniforms = this._uniforms;
len = uniforms.length;
for (i = 0; i < len; ++i) {
uniforms[i].set();
}
// ...
}
而这每一个 uniforms[i],都是一个没有公开在 API 文档中的私有类,也就是接下来 2.3 小节中要介绍的 WebGL Uniform 值封装对象。
2.3. 对 WebGL Uniform 值的封装
进入 createUniforms.js 模块:
// createUniforms.js
UniformFloat.prototype.set = function () { /* ... */ }
UniformFloatVec2.prototype.set = function () { /* ... */ }
UniformFloatVec3.prototype.set = function () { /* ... */ }
UniformFloatVec4.prototype.set = function () { /* ... */ }
UniformSampler.prototype.set = function () { /* ... */ }
UniformInt.prototype.set = function () { /* ... */ }
UniformIntVec2.prototype.set = function () { /* ... */ }
UniformIntVec3.prototype.set = function () { /* ... */ }
UniformIntVec4.prototype.set = function () { /* ... */ }
UniformMat2.prototype.set = function () { /* ... */ }
UniformMat3.prototype.set = function () { /* ... */ }
UniformMat4.prototype.set = function () { /* ... */ }
可以说把 WebGL uniform 的类型都封装了一个私有类。
以表示光方向的 UniformFloatVec3 类为例,看看它的 WebGL 调用:
function UniformFloatVec3(gl, activeUniform, uniformName, location) {
this.name = uniformName
this.value = undefined
this._value = undefined
this._gl = gl
this._location = location
}
UniformFloatVec3.prototype.set = function () {
const v = this.value
if (defined(v.red)) {
if (!Color.equals(v, this._value)) {
this._value = Color.clone(v, this._value)
this._gl.uniform3f(this._location, v.red, v.green, v.blue)
}
} else if (defined(v.x)) {
if (!Cartesian3.equals(v, this._value)) {
this._value = Cartesian3.clone(v, this._value)
this._gl.uniform3f(this._location, v.x, v.y, v.z)
}
} else {
throw new DeveloperError(`Invalid vec3 value for uniform "${this.name}".`);
}
}
2.4. 自动统一值(AutomaticUniforms)
在 2.2 小节中有一个细节没有详细说明,即 ShaderProgram 的 _setUniforms 方法中为什么可以直接调用每一个 uniforms[i] 的 set()?
回顾一下:
Scene.js的render函数内,光的信息被us.update(frameState)更新至UniformState对象中;ShaderProgram的_setUniforms方法,调用uniforms[i].set()方法, 更新每一个私有 Uniform 对象上的值到 WebGL 状态机中
是不是缺少了点什么?
是的,UniformState 的值是如何赋予给 uniforms[i] 的?
这就不得不提及 ShaderProgram.js 模块中为当前着色器对象的 Uniform 分类过程了,查找模块中的 reinitialize 函数:
function reinitialize(shader) {
// ...
const uniforms = findUniforms(gl, program)
const partitionedUniforms = partitionUniforms(
shader,
uniforms.uniformsByName
)
// ...
shader._uniformsByName = uniforms.uniformsByName
shader._uniforms = uniforms.uniform
shader._automaticUniforms = partitionedUniforms.automaticUniforms
shader._manualUniforms = partitionedUniforms.manualUniforms
// ...
}
它把着色器对象上的 Uniform 全部找了出来,并分类为:
_uniformsByName- 一个字典对象,键名是着色器中 uniform 的变量名,值是 Uniform 的封装对象,例如UniformFloatVec3等_uniforms- 一个数组,每个元素都是 Uniform 的封装对象,例如UniformFloatVec3等,若同名,则与_uniformsByName中的值是同一个引用_manualUniforms- 一个数组,每个元素都是 Uniform 的封装对象,例如UniformFloatVec3等,若同名,则与_uniformsByName中的值是同一个引用_automaticUniforms- 一个数组,每个元素是一个 object 对象,表示要 CesiumJS 自动更新的 Uniform 的映射关联关系
举例,_automaticUniforms[i] 用 TypeScript 来描述,是这么一个对象:
type AutomaticUniformElement = {
automaticUniform: AutomaticUniform
uniform: UniformFloatVec3
}
而这个 _automaticUniforms 就拥有自动更新 CesiumJS 内部状态的 Uniform 值的功能,例如我们所需的光状态信息。
来看 AutomaticUniforms.js 模块的默认导出对象:
// AutomaticUniforms.js
const AutomaticUniforms = {
// ...
czm_sunDirectionEC: new AutomaticUniform({ /**/ }),
czm_sunDirectionWC: new AutomaticUniform({ /**/ }),
czm_lightDirectionEC: new AutomaticUniform({ /**/ }),
czm_lightDirectionWC: new AutomaticUniform({ /**/ }),
czm_lightColor: new AutomaticUniform({
size: 1,
datatype: WebGLConstants.FLOAT_VEC3,
getValue: function (uniformState) {
return uniformState.lightColor;
},
}),
czm_lightColorHdr: new AutomaticUniform({ /**/ }),
// ...
}
export default AutomaticUniforms
所以,在 ShaderProgram.prototype._setUniforms 执行的时候,其实是对自动统一值有一个赋值的过程,然后才到各个 uniforms[i] 的 set() 过程:
ShaderProgram.prototype._setUniforms = function (
uniformMap,
uniformState,
validate
) {
let len;
let i;
// ...
const automaticUniforms = this._automaticUniforms;
len = automaticUniforms.length;
for (i = 0; i < len; ++i) {
const au = automaticUniforms[i];
au.uniform.value = au.automaticUniform.getValue(uniformState);
}
// 译者注:au.uniform 实际上也在 this._uniforms 中
// 是同一个引用在不同的位置,所以上面调用 au.automaticUniform.getValue
// 之后,下面 uniforms[i].set() 就会使用的是 “自动更新” 的 uniform 值
const uniforms = this._uniforms;
len = uniforms.length;
for (i = 0; i < len; ++i) {
uniforms[i].set();
}
// ...
}
也许这个过程有些乱七八糟,那就再简单梳理一次:
Scene 的 render 过程中,更新了 uniformState
Context 执行 DrawCommand 过程中,ShaderProgram 的 _setUniforms 执行所有 uniforms 的 WebGL 设置,这其中就会对 CesiumJS 内部不需要手动更新的 Uniform 状态信息进行自动刷新
而在 ShaderProgram 绑定前,早就会把这个着色器中的 uniform 进行分组,一组是常规的 uniform 值,另一组则是需要根据 AutomaticUniform(自动统一值)更新的 uniform 值
说到底,光状态信息也不过是一种 Uniform,在最原始的 WebGL 学习教材中也是如此,只不过 CesiumJS 是一个更复杂的状态机器,需要更多逻辑划分就是了。
3. 在着色器中如何使用
上面介绍完光的类型、在 CesiumJS 源码中如何转化成 Uniform 并刷入 WebGL,那么这一节就简单看看光的状态 Uniform 在着色器代码中都有哪些使用之处。
3.1. 点云
PointCloud.js 使用了 czm_lightColor。
找到 createShaders 函数下面这个分支:
// Version 1.104
function createShaders(pointCloud, frameState, style) {
// ...
if (usesNormals && normalShading) {
vs +=
" float diffuseStrength = czm_getLambertDiffuse(czm_lightDirectionEC, normalEC); \n" +
" diffuseStrength = max(diffuseStrength, 0.4); \n" + // Apply some ambient lighting
" color.xyz *= diffuseStrength * czm_lightColor; \n";
}
// ...
}
显然,这段代码在拼凑顶点着色器代码,在 1.104 版本官方并没有改变这种拼接着色器代码的模式。
着色代码的含义也很简单,将漫反射强度值乘上 czm_lightColor,把结果交给 color 的 xyz 分量。漫反射强度在这里限制了最大值 0.4。
漫反射强度来自内置 GLSL 函数 czm_getLambertDiffuse(参考 packages/engine/Source/Shaders/Builtin/Functions/getLambertDiffuse.glsl)
3.2. 冯氏着色法
Primitive API 材质对象的默认着色方法是 冯氏着色法(Phong),这个在 LearnOpenGL 网站上有详细介绍。
调用链:
MaterialAppearance.js
┗ TexturedMaterialAppearanceFS.js ← TexturedMaterialAppearanceFS.glsl
┗ phong.glsl → vec4 czm_phong()
除了 TexturedMaterialAppearanceFS 外,MaterialAppearance.js 还用了 BasicMaterialAppearanceFS、AllMaterialAppearanceFS 两个片元着色器,这俩也用到了 czm_phong 函数。
看看 czm_phong 函数本体:
// phong.glsl
vec4 czm_phong(vec3 toEye, czm_material material, vec3 lightDirectionEC)
{
// Diffuse from directional light sources at eye (for top-down)
float diffuse = czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 0.0, 1.0), material);
if (czm_sceneMode == czm_sceneMode3D) {
// (and horizon views in 3D)
diffuse += czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 1.0, 0.0), material);
}
float specular = czm_private_getSpecularOfMaterial(lightDirectionEC, toEye, material);
// Temporary workaround for adding ambient.
vec3 materialDiffuse = material.diffuse * 0.5;
vec3 ambient = materialDiffuse;
vec3 color = ambient + material.emission;
color += materialDiffuse * diffuse * czm_lightColor;
color += material.specular * specular * czm_lightColor;
return vec4(color, material.alpha);
}
函数内前面的计算步骤是获取漫反射、高光值,走的是辅助函数,在这个文件内也能看到。
最后灯光 czm_lightColor 和材质的漫反射、兰伯特漫反射、材质辉光等因子一起相乘累加,得到最终的颜色值。
除了 phong.glsl 外,参与半透明计算的 czm_translucentPhong 函数(在 translucentPhong.glsl 文件中)在 OIT.js 模块中用于替换 czm_phong 函数。
3.3. 地球
在 Globe.js 中使用的 GlobeFS 片元着色器代码中使用到了 czm_lightColor,主要是 main 函数中:
void main() {
// ...
#ifdef ENABLE_VERTEX_LIGHTING
float diffuseIntensity = clamp(czm_getLambertDiffuse(czm_lightDirectionEC, normalize(v_normalEC)) * u_lambertDiffuseMultiplier + u_vertexShadowDarkness, 0.0, 1.0);
vec4 finalColor = vec4(color.rgb * czm_lightColor * diffuseIntensity, color.a);
#elif defined(ENABLE_DAYNIGHT_SHADING)
float diffuseIntensity = clamp(czm_getLambertDiffuse(czm_lightDirectionEC, normalEC) * 5.0 + 0.3, 0.0, 1.0);
diffuseIntensity = mix(1.0, diffuseIntensity, fade);
vec4 finalColor = vec4(color.rgb * czm_lightColor * diffuseIntensity, color.a);
#else
vec4 finalColor = color;
#endif
// ...
}
同样是先获取兰伯特漫反射值(使用 clamp 函数钉死在 [0, 1] 区间内),然后将颜色、czm_lightColor、漫反射值和透明度一起计算出 finalColor,把最终颜色值交给下一步计算。
这里区分了两个宏分支,受 TerrainProvider 影响,有兴趣可以追一下 GlobeSurfaceTileProvider.js 模块中 addDrawCommandsForTile 函数中 hasVertexNormals 参数的获取。
3.4. 模型架构中的光着色阶段
在 1.97 大改的 Model API 中,PBR 着色法使用了 czm_lightColorHdr 变量。czm_lightColorHdr 也是自动统一值(AutomaticUniforms)的一个。
在 Model 的更新过程中,有一个 buildDrawCommands 的步骤,其中有一个函数 ModelRuntimePrimitive.prototype.configurePipeline 会增减 ModelRuntimePrimitive 上的着色阶段:
ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) {
// ...
pipelineStages.push(LightingPipelineStage);
// ...
}
上面是其中一个阶段 —— LightingPipelineStage,最后在 ModelSceneGraph.prototype.buildDrawCommands 方法内会调用每一个 stage 的 process 方法,调用 shaderBuilder 构建出着色器对象所需的材料,进而构建出着色器对象。过程比较复杂,直接看其中 LightingPipelineStage.glsl 提供的阶段函数:
void lightingStage(inout czm_modelMaterial material, ProcessedAttributes attributes)
{
// Even though the lighting will only set the diffuse color,
// pass all other properties so further stages have access to them.
vec3 color = vec3(0.0);
#ifdef LIGHTING_PBR
color = computePbrLighting(material, attributes);
#else // unlit
color = material.diffuse;
#endif
#ifdef HAS_POINT_CLOUD_COLOR_STYLE
// The colors resulting from point cloud styles are adjusted differently.
color = czm_gammaCorrect(color);
#elif !defined(HDR)
// If HDR is not enabled, the frame buffer stores sRGB colors rather than
// linear colors so the linear value must be converted.
color = czm_linearToSrgb(color);
#endif
material.diffuse = color;
}
进入 computePbrLighting 函数(同一个文件内):
#ifdef LIGHTING_PBR
vec3 computePbrLighting(czm_modelMaterial inputMaterial, ProcessedAttributes attributes)
{
// ...
#ifdef USE_CUSTOM_LIGHT_COLOR
vec3 lightColorHdr = model_lightColorHdr;
#else
vec3 lightColorHdr = czm_lightColorHdr;
#endif
vec3 color = inputMaterial.diffuse;
#ifdef HAS_NORMALS
color = czm_pbrLighting(
attributes.positionEC,
inputMaterial.normalEC,
czm_lightDirectionEC,
lightColorHdr,
pbrParameters
);
#ifdef USE_IBL_LIGHTING
color += imageBasedLightingStage(
attributes.positionEC,
inputMaterial.normalEC,
czm_lightDirectionEC,
lightColorHdr,
pbrParameters
);
#endif
#endif
// ...
}
#endif
故,存在 USE_CUSTOM_LIGHT_COLOR 宏时才会使用 czm_lightColorHdr 变量作为灯光颜色,参与函数 czm_pbrLighting 计算出颜色值。
3.5. 后记
除了光颜色本身,我在着色器代码中看到被应用的还有光线的方向,主要是 czm_lightDirectionEC 等变量,光照材质仍需一个漫长的学习过程。
CesiumJS 源码杂谈 - 从光到 Uniform的更多相关文章
- 【2020-03-28】Dubbo源码杂谈
前言 本周空闲时间利用了百分之六七十的样子.主要将Dubbo官网文档和本地代码debug结合起来学习,基本看完了服务导出.服务引入以及服务调用的过程,暂未涉及路由.字典等功能.下面对这一周的收获进行一 ...
- CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析
目录 1. 3DTiles 数据集的类型 2. 创建瓦片树 2.1. 请求入口文件 2.2. 创建树结构 2.3. 瓦片缓存机制带来的能力 3. 瓦片树的遍历更新 3.1. 三个大步骤 3.2. 遍历 ...
- CesiumJS 2022^ 源码解读[0] - 文章目录与源码工程结构
很高兴你能在浮躁的年代里还有兴趣阅读源代码,CesiumJS 至今已有十年以上,代码量也积累了三十多万行(未压缩状态). 我也很荣幸自己的文章能被读者看到,如果对你有帮助.有启发,点个赞就是对我最大的 ...
- 【曹工杂谈】Maven源码调试工程搭建
Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...
- CesiumJS 2022^ 源码解读[6] - 三维模型(ModelExperimental)新架构
目录 1. ModelExperimental 的缓存机制 1.1. 缓存池 ResourceCache 1.2. 缓存对象的键设计 ResourceCacheKey 2. 三维模型的加载与解析 2. ...
- 曹工杂谈:为什么很少需要改Spring源码,因为扩展点太多了,说说Spring的后置处理器
前言 最近发了好几篇,都是覆盖框架源码,但是spring的代码,我是从没覆盖过,毕竟,如果方便扩展,没谁想去改源码,而spring就是不需要改源码的那个,真的是"对扩展开放,对修改关闭&qu ...
- Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析
3 Skywalking源码导入 接上文,已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking Agent. 3.1 源码环境搭建 当前最新版本 ...
- ThreeJS 物理材质shader源码分析(顶点着色器)
再此之前推荐一款GLTF物理材质在线编辑器https://tinygltf.xyz/ ThreeJS 物理材质shader源码分析(顶点着色器) Threejs将shader代码分为ShaderLib ...
- OpenCV人脸识别LBPH算法源码分析
1 背景及理论基础 人脸识别是指将一个需要识别的人脸和人脸库中的某个人脸对应起来(类似于指纹识别),目的是完成识别功能,该术语需要和人脸检测进行区分,人脸检测是在一张图片中把人脸定位出来,完成的是搜寻 ...
- AFNetworking 3.0 源码解读 总结(干货)(下)
承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...
随机推荐
- 1js 高级
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- STL练习-看病要排队
题目http://acm.hdu.edu.cn/showproblem.php?pid=1873 看病要排队这个是地球人都知道的常识. 不过经过细心的0068的观察,他发现了医院里排队还是有讲究的 ...
- java文本转语音
下载jar包https://github.com/freemansoft/jacob-project/releases 解压,将jacob-1.18-xxx.dll相应放到项目使用的JAVA_HOME ...
- samba缓存问题
samba 在第一次登录时,会在windows上缓存着登录密码,当你重新修改samba服务端的密码, 再次登录时,windows会自动用缓存的旧密码登录,导致的登录失败.
- 浮动静态路由和BFD联动
浮动静态路由和BFD联动实现路由自动更新 路由器的工作是将数据包从源设备转发到目标设备.在它们之间可能有几个路由器.路由器使用称为路由表的数据库来转发这些数据包.静态路由(Static ...
- ElasticSearch 单点部署
1.下载指定的ES版本(7.17.3) https://www.elastic.co/downloads/past-releases/elasticsearch-7-17-3 2.单点部署elasti ...
- 了解RTT 和RTO 对于TCP 重传的影响
前言 我们已经在很多地方了解TCP 的功能和常用字段.但是TCP 传输发生的异常情况总是让我们很棘手,不知改如何处理.陷入迷茫之中.本文章只针对RTT 和RTO 做了解. 描述 RTT (Round ...
- 微信小程序中如何设置跳转页面
修改project.config.json内容 "cloudfunctionRoot":"cloud", //配置云开发的路径 更改app.js文件内容 App ...
- 使用react-vite-antd,修改antd主题,报错 [vite] Internal server error: Inline JavaScript is not enabled. Is it set in your options? It is hacky way to make this function will be compiled preferentially by less
一般报错 在官方文档中,没有关于vite中如何使用自定义主题的相关配置,经过查阅 1.安装less yarn add less (已经安装了就不必再安装) 2.首先将App.css改成App.les ...
- 学习记录--C++文件读入与存储
C++中对文件操作需要包含头文件<fstream> 操作文件的三大类:1.ofstream写操作 2.ifstream读操作 3.fstream读写操作 一.写文件步骤 1.包含头文件 # ...