CesiumJS 有两种卷帘,一种是 ImageryLayer 的对比,一种是 3DTiles 或 Model 的对比。
官方示例均能找到,目前仅支持左右对比。
1. 核心原理
1.1. 影像图层对比原理
通过控制不同 ImageryLayer 所处的地球瓦片(QuadtreeTile、GlobeSurfaceTile)的透明度实现。
可以在 GlobeFS.glsl
着色器代码中找到这么一段:
#ifdef APPLY_SPLIT
float splitPosition = czm_splitPosition;
// Split to the left
if (split < 0.0 && gl_FragCoord.x > splitPosition) {
alpha = 0.0;
}
// Split to the right
else if (split > 0.0 && gl_FragCoord.x < splitPosition) {
alpha = 0.0;
}
#endif
这段着色器代码的 alpha
变量最终会参与到影像瓦片的颜色着色中,0 即代表不显示。而 czm_splitPosition
即水平分割点(像素坐标),由 canvas 尺寸和 scene.splitPosition
计算而来。
1.2. 模型对比原理
通过控制 Model 的片元是否舍弃实现。
可以在 ModelSplitterStageFS.glsl
找到这么一段(仅限 1.97 新 Model 架构之后):
void modelSplitterStage()
{
// Don't split when rendering the shadow map, because it is rendered from
// the perspective of a totally different camera.
#ifndef SHADOW_MAP
if (model_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard;
if (model_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard;
#endif
}
在渲染 SHADOW_MAP 时,不进行分割(保留完整的遮挡信息)。
2. 影像分割对比
从 1.1 可以看到,除了分割位置 splitPosition
外,还有一个 split
,这个值负责辨别当前片元是分割点左边还是右边的,以方便设置全透明。
2.1. 每个瓦片的分割方向值 - split
这个值作用在 ImageryLayer
上,值类型是 SplitDirection
SplitDirection
是 js 时代没有枚举类型用静态对象,并不是真的就这个类型,真正的 jsdoc/ts 类型还是 SplitDirection 的成员的类型 - number
rightLayer.splitDirection = SplitDirection.RIGHT;
它是怎么传递到着色器中的呢?
2.2. GLSL 函数 - sampleAndBlend 的调用
首先看看在 GlobeFS.glsl
中它的消费之处,是一个函数:
vec4 sampleAndBlend(
// ... 一堆参数
float split,
// ...
)
{
// ...
#ifdef APPLY_SPLIT
float splitPosition = czm_splitPosition;
// Split to the left
if (split < 0.0 && gl_FragCoord.x > splitPosition) {
alpha = 0.0;
}
// Split to the right
else if (split > 0.0 && gl_FragCoord.x < splitPosition) {
alpha = 0.0;
}
#endif
// ...
}
这个函数在 GlobeSurfaceShaderSet.js
模块中被拼接调用:
GlobeSurfaceShaderSet.prototype.getShaderProgram = function (options) {
// ...
if (applySplit) {
fs.defines.push("APPLY_SPLIT");
}
// ...
computeDayColor += "\
color = sampleAndBlend(\n";
// ...
}
2.3. 每个瓦片的分割值 uniform 数组 - u_dayTextureSplit
需要明白的是,在 JS 这边是对着色器代码文本的拼接,所以实际上对 sampleAndBlend
函数的 split 参数传参,其实还没有发生,这里只是拼接出即将传给这个函数执行的变量名,而这个变量名又是代码文本,由 JS 控制:
applySplit ? `u_dayTextureSplit[${i}]` : "0.0"
显然,这个值来自一个数组的元素 u_dayTextureSplit[i]
,从命名风格上看,是一个 glsl 中的 uniform 值,很容易在 GlobeFS.glsl
中找到:
#ifdef APPLY_SPLIT
uniform float u_dayTextureSplit[TEXTURE_UNITS];
#endif
是一个 float 数组,这个 TEXTURE_UNITS
是一个由 GlobeSurfaceShaderSet.js
根据当前视角下有多少个瓦片动态设定的 GLSL 宏,也是一个数字,可以说这个数组记录了每一个地球瓦片是在对比线的左边还是右边。
那这个 u_dayTextureSplit[i]
值又是什么时候设置的呢?
全文搜索,可以在 GlobeSurfaceTileProvider.js
模块中找到 createTileUniformMap
函数,它外抛了一个 uniform 字典对象,里面就有对 u_dayTextureSplit
的设定,来自 dayTextureSplit
属性:
// GlobeSurfaceTileProvider.js
function createTileUniformMap(/* ... */) {
const uniformMap = {
u_dayTextureSplit: function () {
return this.properties.dayTextureSplit;
},
properties: {
// ...
dayTextureSplit: [],
}
};
// ...
return uniformMap;
}
在同一个模块下,找得到设置这个数组的地方,这个函数是每个地球瓦片设置 DC 的地方,相当长:
function addDrawCommandsForTile(tileProvider, tile, frameState) {
// ...
uniformMapProperties.dayTextureSplit[numberOfDayTextures] =
imageryLayer.splitDirection;
applySplit =
applySplit ||
uniformMapProperties.dayTextureSplit[numberOfDayTextures] !== 0.0;
// ...
}
applySplit
即将会在这个函数稍下方调用 2.2 中提及的 getShaderProgram
使用,不细细展开了,有兴趣在模块内搜即可。
显然,这个 u_dayTextureSplit
数组的每个元素值,是根据 imageryLayer.splitDirection
设定的,这数组挂在 createTileUniformMap
返回的 uniformMap
上,也会合并到 DC 的 uniform 值中去,进而着色器程序可以访问到对应的值。
2.4. 分割位置 - splitPosition
这个值既可以作用于 ImageryLayer
,也可以作用于 Model
或 Cesium3DTileset
,是一个 0~1 之间的相对值,相对于 canvas 的像素宽度(当前 CesiumJS,写此文时 1.110 及以下版本仅支持左右分割)。
它是一个自动统一值,表面上是 Scene
的属性,但是实际上是 FrameState
的值:
/**
* Gets or sets the position of the splitter within the viewport. Valid values are between 0.0 and 1.0.
* @memberof Scene.prototype
*
* @type {number}
*/
splitPosition: {
get: function () {
return this._frameState.splitPosition;
},
set: function (value) {
this._frameState.splitPosition = value;
},
},
每一次 render,Scene 都会更新整体的 UniformState:
// Scene.js
function render(scene) {
// ...
us.update(frameState);
// ...
}
在更新时就会把相对值根据 canvas 的宽度计算出绝对像素宽度:
// UniformState.js
UniformState.prototype.update = function (frameState) {
// ...
// Convert the relative splitPosition to absolute pixel coordinates
this._splitPosition =
frameState.splitPosition * frameState.context.drawingBufferWidth;
// ...
}
最终,自动统一值(AutomaticUniforms)会被更新到着色器中,它在这里收集了 uniformState.splitPosition
的值:
// AutomaticUniforms.js
/**
* An automatic GLSL uniform representing the splitter position to use when rendering with a splitter.
* This will be in pixel coordinates relative to the canvas.
*
* @example
* // GLSL declaration
* uniform float czm_splitPosition;
*/
czm_splitPosition: new AutomaticUniform({
size: 1,
datatype: WebGLConstants.FLOAT,
getValue: function (uniformState) {
return uniformState.splitPosition;
},
}),
在以前有文章介绍过 CesiumJS 的统一值,就不详细说自动统一值的更新了。
2.5. 影像分割小结
通过控制瓦片片元的透明度实现分割对比,非常简单。当然,如果读者不熟悉 CesiumJS 地球瓦片的渲染 - 瓦片四叉树的话,可能有点难理解。
每个地球瓦片(GlobeSurfaceTile)都会生产出 TileImagery
,这个 tileImagery 会从每个 ImageryLayer 中读取 ImageryProvider 提供的瓦片源作为纹理,从 terrainProvider 获取地形瓦片作为形状。
所以每个要渲染的地球瓦片一般都有自己的 DrawCommand
(下文简称 DC),体现在 GlobeSurfaceTileProvider
模块下的 addDrawCommandsForTile
函数。
czm_splitPosition
-FrameState
贯穿始终,把分割位置 frameState.splitPosition 传给每个地球瓦片的 DCsplit
-TileImagery
把 ImageryLayer 的 splitDirection 传给 uniform 数组,通过sampleAndBlend
函数传参使用- 最终在
GlobeFS.glsl
中完成这两个值的联合判断,实现片元着色的全透明控制,即影像分割
3. 模型或 3DTiles 的分割对比
普通的模型,在 1.97 版本之后的模型新架构下通过 ModelSplitterStage
这个阶段作用分割值:
// ModelSplitterStageFS.glsl
void modelSplitterStage()
{
// Don't split when rendering the shadow map, because it is rendered from
// the perspective of a totally different camera.
#ifndef SHADOW_MAP
if (model_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard;
if (model_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard;
#endif
}
3.1. 在模型对象上作用分割阶段着色函数与设置分割方向
模型的阶段拼装在以前源码解读的文章讲解过,不赘述。
直达目的地:
ModelSplitterPipelineStage.process = function (/* ... */) {
// ...
shaderBuilder.addDefine(
"HAS_MODEL_SPLITTER",
undefined,
ShaderDestination.FRAGMENT
);
shaderBuilder.addFragmentLines(ModelSplitterStageFS);
const stageUniforms = {};
shaderBuilder.addUniform(
"float",
ModelSplitterPipelineStage.SPLIT_DIRECTION_UNIFORM_NAME,
ShaderDestination.FRAGMENT
);
stageUniforms[
ModelSplitterPipelineStage.SPLIT_DIRECTION_UNIFORM_NAME
] = function () {
return model.splitDirection;
};
renderResources.uniformMap = combine(
stageUniforms,
renderResources.uniformMap
);
}
代码不长,给片元着色器作用分割阶段的着色函数后,立马为 ModelSplitterPipelineStage.SPLIT_DIRECTION_UNIFORM_NAME
这个 uniform 变量 model_splitDirection
设置了统一值,即 model.splitDirection
,意义同 imageryLayer.splitDirection
,可参考 API 文档,SplitDirection
类有明确解释。
3.2. 一个特殊的地方 - 3DTiles 1.0 pnts 动态点云
源码中存在一个类 - TimeDynamicPointCloud
,用到了 PointCloud
类,而 PointCloud
类用到了 Splitter
模块,这个模块作用就是修改片元着色器,与 ModelSplitterPipelineStage
干的事情一致:
const Splitter = {
modifyFragmentShader: function modifyFragmentShader(shader) {
shader = ShaderSource.replaceMain(shader, "czm_splitter_main");
shader +=
// czm_splitPosition is not declared because it is an automatic uniform.
"uniform float czm_splitDirection; \n" +
"void main() \n" +
"{ \n" +
// Don't split when rendering the shadow map, because it is rendered from
// the perspective of a totally different camera.
"#ifndef SHADOW_MAP\n" +
" if (czm_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard; \n" +
" if (czm_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard; \n" +
"#endif\n" +
" czm_splitter_main(); \n" +
"} \n";
return shader;
},
// ...
};
看得出代码都是类似的,只不过代表模型分割方向的 model_splitDirection
变成了 czm_splitDirection
,这个 Splitter
模块作用的 TimeDynamicPointCloud
数据只是一堆 pnts 集合,没有形成一个 Cesium3DTileset。
为什么要单列出来这一点呢?考虑到文章的全面性,列出来方便想修改源码的读者。
4. 总结
分割功能在国内一般称作 “卷帘对比”,通过片元着色器中一个相对简单的逻辑实现,在地球对象和一般模型对象上逻辑略有区别,但是核心思路都是一样的,判断出当前片元所处的 gl_FragCoord
是否需要被 “消失”,或透明,或直接舍弃着色。
官方也提供了相应的例子。
当然,目前它只能在左右方向上对比,可以通过一些修改源码的手段实现上下方向的垂直对比。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。