书接上文。
https://segmentfault.com/a/11…
3. 应用 GLSL 着色器
明确一个定义,在 Primitive API
中利用着色器,实际上是给 Appearance
的 vertexShaderSource
、fragmentShaderSource
或 Material
中的 fabric.source
设置着色器代码,它们所能管制的层级不太一样。然而他们的独特目标都是为了 Geometry 服务的,它们会随着 CesiumJS 的每帧 update 过程,创立 ShaderProgram,创立 DrawCommand,最终去到 WebGL 的底层渲染中。
3.1. 为 Fabric 材质增加自定义着色代码 – Fabric 材质的实质
有了之前的 fabric.uniforms
、fabric.materials
、fabric.components
根底,你可能急不可待想写自定义着色器代码了。须要晓得的一点是,有了 fabric.source
,就不兼容 fabric.components
了,只能二选一。
对于 fabric.uniforms
,它的所有键名都能够在着色器代码中作为 GLSL Uniform 变量应用;对于 fabric.materials
,它的所有键名都能够在着色器代码中作为 GLSL 变量应用,也就是一个计算实现的 czm_material
构造体变量。
编写 fabric.source
,实际上就是写一个函数,它必须返回一个 czm_material
构造体,且输出一些特定的、以后片元的信息:
czm_material czm_getMaterial(czm_materialInput materialInput) {czm_material material = czm_getDefaultMaterial(materialInput);
// ... 一系列解决
return material;
}
czm_material
曾经在之前提及过了,它蕴含了实时渲染所需的一些根本材质参数。而 materialInput
这个变量,它是 czm_materialInput
类型的构造体,定义如下:
struct czm_materialInput {
float s;
vec2 st;
vec3 str;
mat3 tangentToEyeMatrix;
vec3 positionToEyeEC;
vec3 normalEC;
};
其中:
s
– 一维纹理坐标st
– 二维纹理坐标str
– 三维纹理坐标。留神,materialInput.str.st
不肯定就是materialInput.st
,也不能保障materialInput.st.s == materialInput.s
,例如对于椭球体而言,s
是底部到顶部的纹理坐标,st
是经纬度,str
可能是范畴框的轴向值,这要参考源代码tangentToEyeMatrix
– 片元切线空间到眼坐标系的转换矩阵,用于法线计算等positionToEyeEC
– 片元坐标到察看坐标系(眼坐标系)原点的向量,模长为片元到原点的间隔,单位是米,能够用于反射或者折射计算normalEC
– 可用于凹凸映射、反射、折射计算中的眼睛坐标系下的标准化法线
那个 czm_getDefaultMaterial
函数就是获取默认的材质构造,这个函数很简略:
czm_material czm_getDefaultMaterial(czm_materialInput materialInput) {
czm_material material;
material.diffuse = vec3(0.0);
material.specular = 0.0;
material.shininess = 1.0;
material.normal = materialInput.normalEC;
material.emission = vec3(0.0);
material.alpha = 1.0;
return material;
}
有了下面这些根底,你就能够在这个 czm_getMaterial()
函数体里写你想要的片元着色内容了,留神任意 CesiumJS 的内置变量、主动 Uniform、构造体、内置函数都能够用。
3.2. 社区实现案例 – 泛光墙体和流动线材质
参考 前端 3D 引擎 -Cesium 自定义动静材质 – 掘金
有了 3.1 的根底,咱们间接参考网上的一些案例。
const polylinePulseLinkFabric = {
type: 'PolylinePulseLink',
uniforms: {color: Color.fromCssColorString('rgba(0, 255, 255, 1)'),
speed: 0,
image: 'http:/localhost:3000/images/bell.png', // 能够本人指定泛光墙体突变材质
},
source: `czm_material czm_getMaterial(czm_materialInput materialInput) {czm_material material = czm_getDefaultMaterial(materialInput);
// 获取纹理坐标
vec2 st = materialInput.st;
// 对 uniforms.image 的纹理图片进行采样
// 这里须要依据工夫来采样,公式含意读者自行钻研,czm_frameNumber * 0.005 * speed 就是依据内置的
// czm_frameNumber,即以后帧数来代表大抵工夫
vec4 colorImage = texture2D(image, vec2(fract((st.t - speed * czm_frameNumber * 0.005)), st.t));
vec4 fragColor;
fragColor.rgb = color.rgb / 1.0;
fragColor = czm_gammaCorrect(fragColor); // 伽马校对
material.alpha = colorImage.a * color.a;
material.diffuse = (colorImage.rgb + color.rgb) / 2.0;
material.emission = fragColor.rgb;
return material;
}`,
}
// 应用
const wallInstance = new GeometryInstance({
geometry: WallGeometry.fromConstantHeights({
positions: Cartesian3.fromDegreesArray([
97.0, 43.0,
107.0, 43.0,
107.0, 40.0,
97.0, 40.0,
97.0, 43.0,
]),
maximumHeight: 100000.0,
vertexFormat: MaterialAppearance.VERTEX_FORMAT,
}),
})
new Primitive({
geometryInstances: wallInstance,
appearance: new MaterialAppearance({material: new Material({ fabric: polylinePulseLinkFabric}),
}),
})
其用到的突变纹理能够是任意的一个横向色彩至通明的突变 png:
成果:
文中还介绍了 Entity
应用自定义 MaterialProperty
的办法,实际上底层也是 Material
:
class PolylineTrailMaterialProperty {
// ...
getType() {return 'PolylineTrail'}
getValue(time, result) {if (!defined(result)) {result = {}
}
result.color = Property.getValueOrClonedDefault(
this._color,
time,
Color.WHITE,
result.color
)
result.image = this.trailImage
result.time = ((performance.now() - this._time) % this.duration) / this.duration
return result
}
// ... 其余封装参考原文
}
const shader = `czm_material czm_getMaterial(czm_materialInput materialInput) {czm_material material = czm_getDefaultMaterial(materialInput);
vec2 st = materialInput.st;
// 简化版,显然纹理采样的 time 就来自 PolylineTrailMaterialProperty 了,不须要本人管制
vec4 colorImage = texture2D(image, vec2(fract(st.s - time), st.t));
material.alpha = colorImage.a * color.a;
material.diffuse = (colorImage.rgb + color.rgb) / 2.0;
return material;
}`
// 创立一个 'PolylineTrail' 类型的材质对象,并缓存起来:const polylineTrailMaterial = new Material({
fabric: {
type: 'PolylineTrail',
uniforms: {color: new Color(1.0, 0.0, 0.0, 0.5),
image: 'http:/localhost:3000/images/bell.png',
time: 0,
},
source: shader,
}
})
具体的残缺封装调用就不列举了,须要有 Entity API
的应用教训,不在本篇范畴。想晓得 Property 是如何调用底层的,也须要本人钻研 EntityAPI 的底层。
3.3. 间接定义外观对象的两个着色器
fabric.source
只能作用于材质的片元着色,当然也能够通过编写外观对象的两个着色器实现更大自在。
默认状况下,MaterialAppearance
的顶点着色器与片元着色器是这样的:
// GLSL 300 语法,顶点着色器
in vec3 position3DHigh;
in vec3 position3DLow;
in vec3 normal;
in vec2 st;
in float batchId;
out vec3 v_positionEC;
out vec3 v_normalEC;
out vec2 v_st;
void main() {vec4 p = czm_computePosition();
v_positionEC = (czm_modelViewRelativeToEye * p).xyz; // position in eye coordinates
v_normalEC = czm_normal * normal; // normal in eye coordinates
v_st = st;
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}
顶点着色器调用 czm_computePosition()
函数将 position3DHigh
和 position3DLow
合成为 vec4
的模型坐标,而后乘以 czm_modelViewProjectionRelativeToEye
这个内置的矩阵,失去裁剪坐标。而后是片元着色器:
in vec3 v_positionEC;
in vec3 v_normalEC;
in vec2 v_st;
void main()
{
vec3 positionToEyeEC = -v_positionEC;
vec3 normalEC = normalize(v_normalEC);
#ifdef FACE_FORWARD
normalEC = faceforward(normalEC, vec3(0.0, 0.0, 1.0), -normalEC);
#endif
czm_materialInput materialInput;
materialInput.normalEC = normalEC;
materialInput.positionToEyeEC = positionToEyeEC;
materialInput.st = v_st;
czm_material material = czm_getMaterial(materialInput);
#ifdef FLAT
out_FragColor = vec4(material.diffuse + material.emission, material.alpha);
#else
out_FragColor = czm_phong(normalize(positionToEyeEC), material, czm_lightDirectionEC);
#endif
}
如果想齐全定制 Primitive 的着色行为,须要非常相熟你所定制的 Geometry 的 VertexBuffer,也要管制好两大着色器之间互相传递的值。
能够看得出来,Primitive API 应用的材质光照模型是冯氏(Phong)光照模型,可参考根本光照。
案例就不放了,有能力的能够间接参考 CesiumJS 已经推过的一个 3D 风场可视化的案例,它不仅本人写了一个顶点着色器、片元着色器都是自定义的 Appearance,还写了自定义的 Primitive(不是原生 Primitive,是连 DrawCommand 都本人创立的似 Primitive,似 Primitive 将在下文解释)。
3.4. * 源码中如何合并着色器
这段要讲讲源码,定位到 Primitive.prototype.update()
办法:
Primitive.prototype.update = function (frameState) {
const appearance = this.appearance;
const material = appearance.material;
let createRS = false;
let createSP = false;
// 一系列判断是否须要从新创立 ShaderProgram,会批改 createSP 的值
if (createSP) {
const spFunc = defaultValue(
this._createShaderProgramFunction,
createShaderProgram
);
// 默认状况下,会应用 createShaderProgram 函数创立新的 ShaderProgram
spFunc(this, frameState, appearance);
}
};
应用 createShaderProgram
函数会用到外观对象。
function createShaderProgram(primitive, frameState, appearance) {
// ...
// 拆卸顶点着色器
let vs = primitive._batchTable.getVertexShaderCallback()(appearance.vertexShaderSource);
// 从这开始,是给外观对象的片元着色器增加一系列 Buff
vs = Primitive._appendOffsetToShader(primitive, vs);
vs = Primitive._appendShowToShader(primitive, vs);
vs = Primitive._appendDistanceDisplayConditionToShader(
primitive,
vs,
frameState.scene3DOnly
);
vs = appendPickToVertexShader(vs);
vs = Primitive._updateColorAttribute(primitive, vs, false);
vs = modifyForEncodedNormals(primitive, vs);
vs = Primitive._modifyShaderPosition(primitive, vs, frameState.scene3DOnly);
// 拆卸片元着色器
let fs = appearance.getFragmentShaderSource();
fs = appendPickToFragmentShader(fs); // 为片元着色器增加 pick 所需的 vec4 色彩 in(varying) 变量
// 生成 ShaderProgram,并予以校验匹配状况
primitive._sp = ShaderProgram.replaceCache({
context: context,
shaderProgram: primitive._sp,
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
validateShaderMatching(primitive._sp, attributeLocations);
// ...
}
总之,外观的两个着色器也仅仅是 CesiumJS 这个宏大的着色器零碎中的一部分,仍有十分多的状态须要增加到着色器对象(ShaderProgram
)上。
可能通用的 Primitive 就是须要这么多状态附加吧,读者能够自行钻研其它似 Primitive 的着色器创立过程。似 Primitive 将于本文的最初一大节阐明。
4. 底层常识
4.1. 渲染状态对象
留神到一个货色:appearance.renderState
,在创立外观对象时能够传入一个对象字面量:
new MaterialAppearance({
// ...
renderState: {},})
也能够不传递,默认会生成这样一个对象:
{
depthTest: {enabled: true,},
depthMask: false,
blending: BlendingState.ALPHA_BLEND, // 来自 BlendingState 的动态常量成员 ALPHA_BLEND
}
这个对象会记录在外观对象上,随同着 Primitive 的更新过程,还会增增减减、批改状态值,在 Primitive 的 createRenderStates
函数中,用这个对象的即时值创立或获得缓存的 RenderState
实例,期待着在 createCommands
函数中传递给 DrawCommand
。
RenderState
的状态值和 WebGL 最终渲染无关,在 Context
模块的 beginDraw
函数、applyRenderState
函数中,就有大量应用渲染状态的代码(还要往里进去两三层),举例:
function applyDepthMask(gl, renderState) {gl.depthMask(renderState.depthMask);
}
function applyStencilMask(gl, renderState) {gl.stencilMask(renderState.stencilMask);
}
这两个函数就是在批改 WebGL 全局状态的值,值来自 RenderState
实例的 depthMask
和 stencilMask
字段。
CesiumJS 漫长的一帧的更新过程中,有两个状态对象能够关注一下,一个是挂载在 Scene
上的 帧状态 对象(FrameState 实例),另一个就是身处于各个理论三维对象上的 渲染状态 对象(RenderState 实例)。前者记录一些整装待发的资源,例如 DrawCommand
清单等,后者则为三维对象标记在理论渲染时要更改 WebGL 全局状态的状态值。两大状态的链接桥梁是 DrawCommand
。
还有一个贯通于帧更新过程的状态对象:对立值对象(UniformState 实例),是 Context 的成员字段,作用同其名,用于更新要传给着色器的对立值。
4.2. 似 Primitive 对象与创立似 Primitive 对象
这一节介绍的内容将有助于了解 CesiumJS 单帧更新的外围思路。别看 CesiumJS 领有这么多加载数据、模型的 API 类,实际上是能够依据它们在场景构造中的层级,做个简略的分类:
- Entity 与 DataSource,高层级的数据 API,是高级的人类敌对的数据格式加载封装,还能与工夫关联
- Globe 与 ImageryLayer,负责地球自身的渲染,含皮肤(影像 Provider)和肌肉(地形 Provider)
- Primitive 家族,含本篇介绍的
Primitive
,以及glTF
、3DTiles
等数据
Entity 和 DataSource 实际上底层也是在调用 Primitive 家族,只不过这两个属于 Viewer;两头的 Globe 与 ImageryLayer 和最初的 Primitive 家族,属于 Scene 容器。
既然这篇是介绍的 Primitive,那么就重点介绍 Primitive 家族。
你肯定留神过能够向 scene.primitives
这个 PrimitiveCollection
中增加好几种对象:Model
、Cesium3DTileset
、PrimitiveCollection
(是的,能够嵌套增加)、PointPrimitive
、GroundPrimitive
、ClassificationPrimitive
以及本篇介绍的 Primitive
均能够,在 1.101 版本的更新中还增加了一个体素:VoxelPrimitive
(仍在测试)。
我将这类 Primitive 家族类,称为 PrimitiveLike
类,即“似 Primitive”。
这些似 Primitive 有一个共同点,能力增加到 PrimitiveCollection 中,随同着场景的单帧更新过程进入 WebGL 渲染。它们的共同点:
- 有
update
实例办法 - 有
destroy
实例办法
在 update
办法中,它承受 FrameState
对象传入,而后通过本人的渲染逻辑,创立出一系列的指令对象(次要是 DrawCommand
),并送入帧状态对象的指令数组中,待更新结束最终进入 WebGL 的渲染。
所以晓得这些有什么用呢?
Cesium 团队是一个求稳的团队,2012 年还在内测的时候,ES5 规范才落地没多久,哪怕当初的代码也依然是应用函数来创立类,而不是用 ES6 的 Class(只管当初切换过来曾经没什么技术难点了)。ES6 实现类继承是很简略的,然而在那个时候就比拟艰难了。像这种似 Primitive 的状况,ES6 来写实际上就是有一个独特的父类罢了,如果是 TypeScript,那更是能够形象为更轻量的 interface
:
interface PrimitiveLike {update(frameState: FrameState): void
destroy(): void}
这形成了编写自定义 Primitive 的根底,CesiumJS 团队和 CesiumLab 核心成员 vtxf 均有一些古早的材料,通知你如何编写自定义的 Primitive 类。我之前也写有一篇较为相近的、介绍 DrawCommand
并创立繁难自定义三角形 Primitive 的文章,列举如下:
- CesiumJS Wiki – Geometry and Appearance
- cesiumlab vtxf – cesium-custom-primitive
- [知乎 – Cesium DrawCommand [1] 不谈地球 画个三角形](https://zhuanlan.zhihu.com/p/…)
驰名的 Cesium 3D 风场案例就是一个十分经典的利用:
- GPU Powered Wind Visualization With Cesium – Cesium 历史博客
4.3. Primitive 在 Scene 中的大抵图示
如果读过我写的源码系列,应该晓得 Primitive 在 Scene 的更新地位(Scene
模块下的 render
函数),简略放个图吧:
这样就能大抵看到在什么时候更新的 PrimitiveCollection 了。
有趣味理解源码渲染架构的能够去补补我之前写的系列。
文末小结
这两篇文章汇合了我三年前的几篇不成熟的文章,我终于系统地写出了这几个内容:
-
一般性的
Primitive API
用法,包含Geometry API
的自定义几何、参数内置几何Appearance + Material API
所表白的 CesiumJS Fabric 材质标准
- 提出似 Primitive 的概念,为之后自定义 Primitive 学习挡在 WebGL 原生接口之前的最底层 API 打下基础
- 简略思考了 CesiumJS 的着色器设计和利用
心愿对读者有用。