本篇波及到的所有接口在公开文档中均无,须要下载 GitHub 上的源码,本人创立公有类的文档。
npm run generateDocumentation -- --private
yarn generateDocumentation -- --private
pnpm generateDocumentation -- --private
本篇当然不会波及着色器算法解说。
1. 对 WebGL 接口的封装
任何一个有谋求的 WebGL 3D 库都会封装 WebGL 原生接口。CesiumJS 从外部封测到当初,曾经有十年了,WebGL 自 2011 年公布以来也有 11 年了,这期间小修小补不可避免。
更何况 CesiumJS 是一个 JavaScript 的天文 3D 框架,它在源代码设计上具备两大特色:
- 面向对象
- 模块化
对于模块化策略,CesiumJS 在 1.63 版本曾经从 require.js
切换到原生 es-module
格局了。而 WebGL 是一种应用全局状态的指令式格调接口,改为面向对象格调就必须做封装。ThreeJS 是通用 Web3D 库中做 WebGL 封装的代表作品。
封装有另外的益处,就是底层 WebGL 接口在这十多年中的变动,能够在封装后屏蔽掉这些变动,下层利用调用封装后的 API 形式根本不变。
1.1. 缓冲对象封装
CesiumJS 封装了 WebGLBuffer
以及 WebGL 2.0 才正式反对(1.0 中用扩大)的 VAO,别离封装成了 Buffer
类和 VertexArray
类。
Buffer
类比较简单,提供了简略工厂模式的动态创立办法:
// 创立存储顶点缓冲对象
Buffer.createVertexBuffer = function (options) {
// ...
return new Buffer({
context: options.context,
bufferTarget: WebGLConstants.ARRAY_BUFFER,
typedArray: options.typedArray,
sizeInBytes: options.sizeInBytes,
usage: options.usage,
});
};
// 创立顶点索引缓冲对象
Buffer.createIndexBuffer = function (options) {// ...};
Buffer
对象在实例化时,就会创立 WebGLBuffer
并将类型数组上载:
// Buffer 构造函数中
const buffer = gl.createBuffer();
gl.bindBuffer(bufferTarget, buffer);
gl.bufferData(bufferTarget, hasArray ? typedArray : sizeInBytes, usage);
gl.bindBuffer(bufferTarget, null);
除了这两个用于创立的静态方法,还有一些拷贝缓冲对象的办法,就不一一列举了。
留神一点:Buffer
对象不保留原始顶点类型数组数据。 这一点是出于节约 JavaScript 内存思考。
而顶点数组对象 VertexArray
,封装的则是 OpenGL 系中的一个数据模型 VertexArrayObject
,在 WebGL 中是用意节约设置多个顶点缓冲到全局状态对象的性能损耗。
创立 CesiumJS 的顶点数组对象也很简略,只需按 WebGL 的顶点属性(Vertex Attribute)的格局去拆卸 Buffer
对象即可:
const positionBuffer = Buffer.createVertexBuffer({
context: context,
sizeInBytes: 12,
usage: BufferUsage.STATIC_DRAW
})
const normalBuffer = Buffer.createVertexBuffer({
context: context,
sizeInBytes: 12,
usage: BufferUsage.STATIC_DRAW
})
const attributes = [
{
index: 0,
vertexBuffer: positionBuffer,
componentsPerAttribute: 3,
componentDatatype: ComponentDatatype.FLOAT
},
{
index: 1,
vertexBuffer: normalBuffer,
componentsPerAttribute: 3,
componentDatatype: ComponentDatatype.FLOAT
}
]
const va = new VertexArray({
context: context,
attributes: attributes
})
你如果在对着上述代码练习,你必定没法胜利创立,并发现一个问题:没有 context
参数传递给 Buffer
或 VertexArray
,因为 context
对象(类型 Context
)是 WebGL 渲染上下文对象等底层接口的的封装对象,没有它无奈创立 WebGLBuffer
等原始接口对象。
所以,Buffer
、VertexArray
并不是孤立的 API,必须与其它封装一起搭配来用,它们两个至多要依赖 Context
对象才行,在 1.4 中会介绍如何应用 Context
类封装 WebGL 底层接口并如何拜访 Context
对象的。
很少有须要间接创立 Buffer
、VertexArray
的时候,应用这两个接口,就意味着你取得的数据合乎 VBO
格局,其它人类浏览敌对型的数据格式必须转换为 VBO 格局能力间接用这俩类。如果你须要应用第 2 节中提及的指令对象,这两个类就能派上用场了。
1.2. 纹理与采样参数封装
纹理是 WebGL 中一个非常复杂的话题。
先说采纳参数吧,在 WebGL 1.0 时还没有原生的采样器 API,到 2.0 才推出的 WebGLSampler
接口。所以,CesiumJS 封装了一个简略的 Sampler
类:
function Sampler(options) {
// ...
this._wrapS = wrapS;
this._wrapT = wrapT;
this._minificationFilter = minificationFilter;
this._magnificationFilter = magnificationFilter;
this._maximumAnisotropy = maximumAnisotropy;
}
其实就是把 WebGL 1.0 中的纹理采样参数做成了一个对象,没什么难的。
纹理类 Texture
则是对 WebGLTexture
的封装,它不仅封装了 WebGLTexture
,还封装了数据上载的性能,只需安心地把贴图数据传入即可。
同 Buffer
、VertexArray
,Texture
也要 context
参数。
import {Texture, Sampler,} from 'cesium'
new Texture({
context: context,
width: 1920,
height: 936,
source: new Float32Array([/* ... */]), // 0~255 灰度值的 RGBA 图像数据
// 可选采样参数
sampler: new Sampler()})
你能够在 ImageryLayer.js
模块中找到创立影像瓦片纹理的代码:
ImageryLayer.prototype._createTextureWebGL = function (context, imagery) {
// ...
return new Texture({
context: context,
source: image,
pixelFormat: this._imageryProvider.hasAlphaChannel
? PixelFormat.RGBA
: PixelFormat.RGB,
sampler: sampler,
});
}
除了创立纹理,CesiumJS 还提供了纹理的拷贝工具函数,譬如从帧缓冲对象中拷贝出一个纹理:
Texture.fromFramebuffer = function (/* ... */) {/* ... */}
Texture.prototype.copyFromFramebuffer = function (/* ... */) {/* ... */}
或者创立 mipmap:
Texture.prototype.generateMipmap = function (/* ... */) {/* ... */}
1.3. 着色器封装
家喻户晓,WebGL 的着色器相干 API 是 WebGLShader
和 WebGLProgram
,顶点着色器和片元着色器独特形成一个着色器程序对象。在一帧的渲染中,由多个通道形成,每个通道在触发 draw
动作之前,通常要切换着色器程序,以达到不同的计算成果。
CesiumJS 的渲染远远简单于通用 Web3D,意味着有大量着色器程序。对象多了,就要治理。CesiumJS 封装了无关底层 API 的同时,还设计了缓存机制。
CesiumJS 应用 ShaderSource
类来治理着色器代码文本,应用 ShaderProgram
类来治理 WebGLProgram
和 WebGLShader
,应用 ShaderCache
类来缓存 ShaderProgram
,再应用 ShaderFunction
、ShaderStruct
、ShaderDestination
来辅助 ShaderSource
解决着色器代码文本中的 glsl 函数、构造体成员、宏定义。
此外,还有一个 ShaderBuilder
类来辅助 ShaderProgram
的创立。
这一堆公有类与后面 Buffer
、VertexArray
、Texture
一样,并不能独自应用,通常是与第 2 节中的各种指令对象一起用。
上面给出一个例子,它应用 ShaderProgram
的静态方法 fromCache
创立着色器程序对象,这个办法会创建对象的同时并缓存到 ShaderCache
对象中,有趣味的能够自行查看缓存的代码。
const vertexShaderText = `attribute vec3 position;
void main() {gl_Position = czm_projection * czm_view * czm_model * vec4(position, 1.0);
}`
const fragmentShaderText = `uniform vec3 u_color;
void main(){gl_FragColor = vec4(u_color, 1.0);
}`
const program = ShaderProgram.fromCache({
context: context,
vertexShaderSource: vertexShaderText,
fragmentShaderSource: fragmentShaderText,
attributeLocations: {"position": 0,},
})
残缺例子能够找我之前的写的对于应用 DrawCommand
绘制三角形的文章。
1.4. 上下文对象与渲染通道
WebGL 底层接口的封装,基本上都在 Context
类中。最外围的就是渲染上下文(WebGLRenderingContext
、WebGL2RenderingContext
)对象了,除此之外,Context
上还有一些重要的渲染相干的性能和成员变量:
- 一系列 WebGL 2.0 中才反对的、WebGL 1.0 中用扩大才反对的个性
- 压缩纹理的反对信息
UniformState
对象PassState
对象RenderState
对象- 参加帧渲染的性能,譬如 draw、readPixels 等
- 创立拾取用的 PickId
- 操作、校验
Framebuffer
对象
通常,通过 Scene
对象上的 FrameState
对象,即可拜访到 Context
对象。
WebGL 渲染上下文对象裸露的常量很多,CesiumJS 把渲染上下文上的常量以及可能会用到的常量都封装到 WebGLConstants.js
导出的对象中了。
还有一个货色要特地阐明,就是通道,WebGL 是没有通道 API 的,而一帧之内切换着色器进行多道绘制过程是很常见的事件,每一道触发 draw
的行为,叫做通道。
CesiumJS 把高层级三维对象的渲染行为做了打包,封装成了三类指令对象,在第 2 节中会讲;这些指令对象是有先后优先级的,CesiumJS 把这些优先级形容为通道,应用 Pass.js
导出的枚举来定义,目前指令对象有 10 个优先级:
const Pass = {
ENVIRONMENT: 0,
COMPUTE: 1,
GLOBE: 2,
TERRAIN_CLASSIFICATION: 3,
CESIUM_3D_TILE: 4,
CESIUM_3D_TILE_CLASSIFICATION: 5,
CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
OPAQUE: 7,
TRANSLUCENT: 8,
OVERLAY: 9,
NUMBER_OF_PASSES: 10,
};
NUMBER_OF_PASSES
成员代表以后有 10 个优先级。
而在帧状态对象上,也有一个 passes
成员:
// FrameState 构造函数
this.passes = {
render: false,
pick: false,
depth: false,
postProcess: false,
offscreen: false,
};
这 5 个布尔值就管制着渲染用的是哪个通道。
指令对象的通道状态值,加上帧状态对象上的通道状态,独特形成了 CesiumJS 宏大的形象模型中的“通道”概念。
其实我认为这样设计会导致 Scene 单帧渲染时有大量的 if 判断、排序解决,显得有些冗余,能像 WebGPU 这种新 API 一样提供通道编码器或者会简化通道的概念。
1.5. 对立值(uniform)封装
对立值,即 WebGL 中的 Uniform
,不相熟的读者须要本人先学习 WebGL 相干概念。
每一帧,有大量的状态值是和上一帧不一样的,也就是须要随时更新进着色器中。CesiumJS 为此做出了封装,这种频繁变动的对立值被封装进了 AutomaticUniforms
对象中了,每一个成员都是 AutomaticUniform
类实例:
// AutomaticUniforms.js 中
function AutomaticUniform(options) {
this._size = options.size;
this._datatype = options.datatype;
this.getValue = options.getValue;
}
从默认导出的 AutomaticUniforms
对象中拿一个成员来看:
czm_projection: new AutomaticUniform({
size: 1,
datatype: WebGLConstants.FLOAT_MAT4,
getValue: function (uniformState) {return uniformState.projection;},
})
这个对立值是摄像机的投影矩阵,它的取值函数须要一个 uniformState
参数,也就是实时地从对立值状态对象(类型 UniformState
)上获取的。
Context
对象领有一个只读的 UniformState
getter,指向一个公有的成员。当 Scene
在执行帧状态上的指令列表时,会调用 Context
的绘制函数,进一步地会调用 Context.js
模块内的 continueDraw
函数,它就会执行着色器程序对象的 _setUniforms
办法:
shaderProgram._setUniforms(
uniformMap,
context._us,
context.validateShaderProgram
);
这个函数就能把指令对象传下来的自定义 uniformMap
,以及 AutomaticUniforms
给设置到 ShaderProgram
内置的 WebGLProgram
上,也就是实现着色器内对立值的设置。
1.6. 渲染容器封装
渲染容器次要就是指帧缓冲对象、渲染缓冲对象。
渲染缓冲对象,CesiumJS 封装为 Renderbuffer
类,是对 WebGLRenderbuffer
的一个非常简单的封装,不细说了,然而要独自提一点,若启用了 msaa,会调用相干的绑定函数:
// Renderbuffer.js
function Renderbuffer(options) {
// ...
const gl = context._gl;
// ...
this._renderbuffer = this._gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderbuffer);
if (numSamples > 1) {
gl.renderbufferStorageMultisample(
gl.RENDERBUFFER,
numSamples,
format,
width,
height
);
} else {gl.renderbufferStorage(gl.RENDERBUFFER, format, width, height);
}
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
接下来说帧缓冲的封装。
一般的帧缓冲,也就是惯例的 WebGLFramebuffer
被封装到 Framebuffer
类里了,它有几个数组成员,用于保留帧缓冲用到的色彩附件、深度模板附件的容器纹理、容器渲染缓冲。
function Framebuffer(options) {
const context = options.context;
//>>includeStart('debug', pragmas.debug);
Check.defined("options.context", context);
//>>includeEnd('debug');
const gl = context._gl;
const maximumColorAttachments = ContextLimits.maximumColorAttachments;
this._gl = gl;
this._framebuffer = gl.createFramebuffer();
this._colorTextures = [];
this._colorRenderbuffers = [];
this._activeColorAttachments = [];
this._depthTexture = undefined;
this._depthRenderbuffer = undefined;
this._stencilRenderbuffer = undefined;
this._depthStencilTexture = undefined;
this._depthStencilRenderbuffer = undefined;
// ...
}
用也很简略,调用原型链上绑定相干的办法即可。CesiumJS 反对 MRT,所以有一个对应的 bindDraw
办法:
Framebuffer.prototype.bindDraw = function () {
const gl = this._gl;
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._framebuffer);
};
msaa 则用到了 MultisampleFramebuffer
这个类;CesiumJS 还设计了 FramebufferManager
类来治理帧缓冲对象,在后处理、OIT、拾取、Scene 的帧缓冲治理等模块中均有应用。
2. 三类指令
CesiumJS 并不会间接解决天文三维对象,而是在各种更新的流程管制函数中,由每个三维对象去生成一种叫做“指令”的对象,送入帧状态对象的相干渲染队列中。
这些指令对象的就屏蔽了各种高层级的“人类敌对”型三维数据对象的差别,Context
能不便对立地解决它们携带的数据资源(缓冲、纹理)和行为(着色器)。
这些指令对象分成三类:
- 绘图指令(绘制指令),
DrawCommand
类,负责渲染绘图 - 清屏指令,
ClearCommand
类,负责清空绘图区域 - 通用计算指令,
ComputeCommand
类,用 WebGL 来进行 GPU 并行计算
上面进行简略解说。
2.1. 绘图指令(绘制指令)
也就是 DrawCommand
类,位于 Renderer/DrawCommand.js
模块。
绘图指令,在 Scene
对象的每一帧更新过程中,由各种高级三维对象生成,并增加到帧状态对象中,期待渲染。
我已经写过一篇 DrawCommand
画最简略的三角形的文,如果你点进我的用户文章列表找不到,你可能看到本文的盗版了:)
简而言之,创立 DrawCommand
须要数据(VertexArray
、uniformMap、RenderState
),也须要行为(ShaderProgram
)。创立 DrawCommand
这些辅料,大多数都须要 Context
对象。
它的执行过程如下:
DrawCommand.prototype.execute
Context.prototype.draw
fn beginDraw
fn continueDraw
对于 Scene
渲染一帧的文章中曾经提过如何执行这些绘制指令,是 Scene
原型链上的 updateAndExecuteCommands
办法登程,一路走到 executeCommand
函数,最终调用各种指令对象的执行办法。
下面的繁难逻辑流程中,beginDraw
这个模块内的函数,负责绑定帧缓冲对象和渲染状态,并绑定 ShaderProgram
到 WebGL 全局状态上:
function beginDraw(/* ... */) {
// ...
bindFramebuffer(context, framebuffer);
applyRenderState(context, renderState, passState, false);
shaderProgram._bind();
// ...
}
紧接着,continueDraw
函数会向 WebGL 全局状态设置(更新)对立值:
// function continueDraw 中
shaderProgram._setUniforms(
uniformMap,
context._us,
context.validateShaderProgram
);
而后就是走 WebGL 的惯例绘制流程了,绑定 VertexArray
,判断是否用了索引缓冲,岔开逻辑别离绘制顶点数据:
// function continueDraw 中
va._bind();
const indexBuffer = va.indexBuffer;
if (defined(indexBuffer)) {// ...} else {count = defaultValue(count, va.numberOfVertices);
if (instanceCount === 0) {context._gl.drawArrays(primitiveType, offset, count);
} else {
context.glDrawArraysInstanced(
primitiveType,
offset,
count,
instanceCount
);
}
}
va._unBind();
代码 va._bind();
是在绑定各种顶点数据。
2.2. 清屏指令
清屏指令,与 WebGL 的 clear
办法目标是一样的,即清空以后帧缓冲(或 canvas)的色彩局部、深度模板局部,并填充特定的值,封装成了 ClearCommand
类。
清屏指令就比较简单了,它的执行与绘制指令是一个过程,都在 Scene.js
模块下的 executeCommand
函数中:
// function executeCommand 中
if (command instanceof ClearCommand) {command.execute(context, passState);
return;
}
能够看到它一旦被执行,就不继续执行前面的对于绘制指令的代码,间接 return 了。
紧接着会执行 Context
原型链上的 clear
办法,它也会绑定帧缓冲、设置渲染状态,最初调用 gl.clear
办法,将设定的待革除后要填充的色彩、深度、模板值刷上去,过程比较简单,就不贴源码了。
Scene
对象上有几个清屏指令成员对象,在渲染流程中由 updateAndClearFramebuffers
函数执行色彩清屏指令,而模板与深度的清屏指令则由 executeCommands
函数执行:
// Scene.js 模块下
function executeCommandsInViewport(/* ... */) {
// ...
if (firstViewport) {if (defined(backgroundColor)) {updateAndClearFramebuffers(scene, passState, backgroundColor);
}
// ...
}
executeCommands(scene, passState);
}
function updateAndClearFramebuffers(scene, passState, clearColor) {
// ...
const clear = scene._clearColorCommand;
Color.clone(clearColor, clear.color);
clear.execute(context, passState);
// ...
}
function executeCommands(scene, passState) {
// ...
const clearDepth = scene._depthClearCommand;
const clearStencil = scene._stencilClearCommand;
// ...
for (let i = 0; i < numFrustums; ++i) {
// ...
clearDepth.execute(context, passState);
if (context.stencilBuffer) {clearStencil.execute(context, passState);
}
// ... 执行其它 commands 的分支逻辑
}
}
当然,有 ClearCommand
的对象不仅仅只有 Scene
,其它中央也有,你能够在源码中全局搜寻 new ClearCommand
关键词。
2.3. 通用计算指令
晚期的 WebGL 1.0 对 GPU 通用计算(GPGPU)反对得并不是很好,想让 GPU 模仿一般的并行计算,须要把数据编码成纹理,经由渲染管线实现纹理采样、计算后,再输入到帧缓冲对象上,再应用 WebGL 读像素的接口函数把后果读取进去。
WebGL 2.0 的计算着色器是捷足先登。
CesiumJS 最开始应用了 ComputeCommand
来区别作用于渲染工作的 DrawCommand
。
CesiumJS 源码中应用了计算指令的中央不多,最经典的就是影像图层的重投影办法中:
ImageryLayer.prototype._reprojectTexture = function (/**/) {
// ...
const computeCommand = new ComputeCommand({
persists: true,
owner: this,
preExecute: function (command) {reprojectToGeographic(command, context, texture, imagery.rectangle);
},
postExecute: function (outputTexture) {
imagery.texture = outputTexture;
that._finalizeReprojectTexture(context, outputTexture);
imagery.state = ImageryState.READY;
imagery.releaseReference();},
canceled: function () {
imagery.state = ImageryState.TEXTURE_LOADED;
imagery.releaseReference();},
});
// ...
}
执行它的“管家”,不是 Context
而是 ComputeEngine
类。
ComputeCommand.prototype.execute = function (computeEngine) {computeEngine.execute(this);
};
当然,去看 ComputeEngine
类的构造函数,它也只不过是对 Context
的一个封装,用到了装璜器模式:
function ComputeEngine(context) {this._context = context;}
查看 ComputeEngine.prototype.execute
的外围执行局部,其实它也是用 DrawCommand
和 ClearCommand
,在独立的 Framebuffer
上执行传入的 ShaderProgram
。
ComputeEngine.prototype.execute = function (computeCommand) {
// ...
const outputTexture = computeCommand.outputTexture;
const width = outputTexture.width;
const height = outputTexture.height;
const context = this._context;
const vertexArray = defined(computeCommand.vertexArray)
? computeCommand.vertexArray
: context.getViewportQuadVertexArray();
const shaderProgram = defined(computeCommand.shaderProgram)
? computeCommand.shaderProgram
: createViewportQuadShader(context, computeCommand.fragmentShaderSource);
// 应用 outputTexture 作为 fbo 的绘制后果载体
const framebuffer = createFramebuffer(context, outputTexture);
const renderState = createRenderState(width, height);
const uniformMap = computeCommand.uniformMap;
// 应用模块内的变量实现 fbo 清屏
const clearCommand = clearCommandScratch;
clearCommand.framebuffer = framebuffer;
clearCommand.renderState = renderState;
clearCommand.execute(context);
// 应用模块内的变量实现 fbo 渲染管线执行
const drawCommand = drawCommandScratch;
drawCommand.vertexArray = vertexArray;
drawCommand.renderState = renderState;
drawCommand.shaderProgram = shaderProgram;
drawCommand.uniformMap = uniformMap;
drawCommand.framebuffer = framebuffer;
drawCommand.execute(context);
framebuffer.destroy();
// ...
}
具体的计算着色、纹理编码原理就不介绍了,属于着色器原理,本文(本系列文章)更多是介绍架构设计细节。
3. 自定义着色器
CesiumJS 留有一些公开的 API,容许开发者写本人的着色过程。
在 Cesium 团队鼎力开发下一代 3DTiles 和模型类新架构之前,这部分能力比拟弱,只有一个 Fabric
材质标准能写写现有几何对象的材质成果,且文档较少。
随着下一代 3DTiles 与新的模型类实验性启用后,带来了自由度更高的 CustomShader API
,不仅仅有齐全的文档,而且给到开发者最大的自在去批改图形渲染。
3.1. 晚期 Fabric 材质标准中的自定义着色器
写 Primitive API
时,有这么一个字段:
new Primitive({
//...
appearance: new MaterialAppearance({material: Material.fromType('Color'),
faceForward: true
})
})
这个 MaterialAppearance
是 Appearance
类的一个子类,除了上述这两个属性之外,还能够本人传递顶点着色器代码。
然而,通常不会间接向 Appearance
的派生子类们提供顶点着色器、片元着色器,因为外观对象所需的着色器有额定的要求,通常是创立 Material
时写材质 glsl 函数:
const fabricMaterial = new Material({
fabric: {
uniforms: {my_var: 0.5,},
source: `czm_material czm_getMaterial(czm_materialInput input) {czm_material material = czm_getDefaultMaterial(input);
material.diffuse = vec3(materialInput.st, 0.0);
material.alpha = my_var;
return material;
}`
}
})
而后把这个遵循了 Fabric
材质标准的材质对象,传递给外观对象:
new MaterialAppearance({material: fabricMaterial})
Fabric
材质标准这里不多介绍,当前有机会再开一文吧,简略的说传递一个 JavaScript 对象给 Material
的 fabric
成员变量即可,这个对象能够自定义一种材质,能够具备 uniformMap,并在 glsl 代码中应用一个函数,返回一个 czm_material
构造体作为材质。
尽管能够创立 glsl 构造体作为材质,然而它仅仅只能作用于片元的局部着色过程。
Appearance API
作用的是 Primitive API
生成的图元对象,外观对象反对间接传递 Primitive 所需的两大着色器代码,然而也是有限度的,一些 vertex attritbute
、varying
是必须存在的,而且还得本人解决渲染管线的转换,这方面材料较少。
通过下列代码,你能够输入最简略的 MaterialAppearance
对象生成的内置默认两大着色器代码,不便本人批改:
const appearance = new Cesium.MaterialAppearance({material: new Cesium.Material({}),
})
const vs = appearance.vertexShaderSource
const fs = appearance.fragmentShaderSource
const fsWithFabricMaterial = appearance.getFragmentShaderSource()
// 打印这三个变量,都是 glsl 代码字符串
// console.log(vs, fs, fsWithFabricMaterial)
3.2. 后处理中的自定义着色器
CesiumJS 其实内置了一大堆常见的后处理器,常见的辉光(Bloom)、疾速抗锯齿(FXAA)等都有,参考 PostProcessStageLibrary
这个类导出的动态字段即可。
尽管这些后处理都是最根本的、对整个 FBO 进行的,成果个别。
你能够拜访 scene.postProcessStages
拜访后处理器的容器,譬如启用疾速抗锯齿是这样启用的:
viewer.scene.postProcessStages.fxaa.enabled = true
内置的环境光遮蔽(AO)或辉光(Bloom)必然在所有后处理器之前执行,FXAA 必然位于所有后处理器之后执行。这三个阶段也是 CesiumJS 默认创立的后处理器。
你能够本人创立独自的后处理阶段(PostProcessStage
)或合成后处理阶段(PostProcessStageComposite
)作为一个后处理器传入 PostProcessStageCollection
容器中,如果不应用官网提供的其它常见后处理算法,那么你也能够本人写着色器。
官网文档写道:
每个后处理阶段的输出纹理,是 Scene 渲染的纹理,或前一后处理阶段的输入纹理。
参考 PostProcessStage
类的文档,官网也提供了两个例子:
// 例子 1,粗犷地批改色彩
const fs = `uniform sampler2D colorTexture;
varying vec2 v_textureCoordinates;
uniform float scale;
uniform vec3 offset;
void main() {vec4 color = texture2D(colorTexture, v_textureCoordinates);
gl_FragColor = vec4(color.rgb * scale + offset, 1.0);
}`
scene.postProcessStages.add(new Cesium.PostProcessStage({
fragmentShader: fs,
uniforms: {
scale: 1.1,
offset: function() {return new Cesium.Cartesian3(0.1, 0.2, 0.3);
}
}
}))
后处理还反对拾取对象的判断,也就是例子 2,批改拾取对象的色彩:
const fs = `uniform sampler2D colorTexture;
varying vec2 v_textureCoordinates;
uniform vec4 highlight;
void main() {vec4 color = texture2D(colorTexture, v_textureCoordinates);
if (czm_selected()) {
vec3 highlighted =
highlight.a * highlight.rgb + (1.0 - highlight.a) * color.rgb;
gl_FragColor = vec4(highlighted, 1.0);
} else {gl_FragColor = color;}
}`
const stage = scene.postProcessStages.add(new Cesium.PostProcessStage({
fragmentShader: fs,
uniforms: {highlight: function() {return new Cesium.Color(1.0, 0.0, 0.0, 0.5);
}
}
}))
stage.selected = [cesium3DTileFeature]
PostProcessStage
的 selected
是一个 js 数组,反对设定 Cesium3DTileFeature
、Label
、Billboard
等具备 pickId
拜访器,或 Model
、Cesium3DTilePointFeature
等具备 pickIds
拜访器的类为“选中对象”,并在更新过程(PostProcessStage.prototype.update
)中创立一张抉择纹理。
无关后处理的材料当前可能会专门写利用篇来介绍。
3.3. 新架构带来的 CustomShader API
这个随同着 ModelExperimental
这个新架构(2022 年 5 月,此架构正在启动对原 Model
类相干架构的替换)的更新而随时可能会更新。
目前,CustomShader API
仅反对在 Cesium3DTileset
、ModelExperimental
两个类上应用。传入的自定义着色器作用在每个瓦片或模型上。
举例:
import {CustomShader, UniformType, TextureUniform, VaryingType} from 'cesium'
const customShader = new CustomShader({
uniforms: {
u_colorIndex: {
type: UniformType.FLOAT,
value: 1.0
},
u_normalMap: {
type: UniformType.SAMPLER_2D,
value: new TextureUniform({url: "http://example.com/normal.png"})
}
},
varyings: {v_selectedColor: VaryingType.VEC3},
vertexShaderText: `void vertexMain(
VertexInput vsInput,
inout czm_modelVertexOutput vsOutput
) {
v_selectedColor = mix(
vsInput.attributes.color_0,
vsInput.attributes.color_1, u_colorIndex
);
vsOutput.positionMC += 0.1 * vsInput.attributes.normal;
}`,
fragmentShaderText: `void fragmentMain(
FragmentInput fsInput,
inout czm_modelMaterial material
) {
material.normal = texture2D(u_normalMap, fsInput.attributes.texCoord_0);
material.diffuse = v_selectedColor;
}`
})
相干标准文档能够在源代码根目录下 Documentation/CustomShaderGuide/README.md
文件中查阅。
你能够设定在渲染管线中须要的 uniformMap,指定两个着色器之间的替换值(varyings),并且能在两大着色器中拜访 CesiumJS 封装好的两个构造体 VertexInput
、FragmentInput
,它们为你提供了尽可能详尽的值,譬如:
- 顶点属性(Vertex Attributes)
- 因素 / 批次 ID(FeatureID/BatchID)
- 3DTiles 1.1 标准中的属性元数据(Metadata)
顶点属性中提供了尽可能详尽的顶点信息,常见的:顶点坐标、法线、纹理坐标、色彩等均有附带,而且附带了各种坐标系下的值。譬如,你能够在顶点着色器中这样拜访相机坐标系下的顶点坐标:
void vertexMain(
VertexInput vsInput,
inout czm_modelVertexOutput vsOutput
) {
vsOutput.positionMC = czm_projection * (vec4(vsInput.attributes.positionEC) + vec4(.0, .0, 10.0, 0.0)
);
}
上述代码将察看坐标的 z 值进步了 10 个单位,最初乘上内置的投影矩阵,作为输入模型坐标。
CesiumJS 提供的所有内置 glsl 函数、常量、主动变量均反对在 CustomShader API 中应用。
这无疑给想批改模型形态、模型成果的开发者们提供了极大的便当。
4. 总结
集体感觉在 WebGL 封装这部分,在本文曾经讲得能够了,更高层级的利用封装,例如 OIT、GPUPick、各种对象的着色器和指令生成过程,均基于这篇文章的内容,均产生在 Scene
的渲染流程中。
最初再强调一下,本文仅是对架构方面的介绍,而不是着色器算法的详解,着色器算法堪称 CesiumJS 的头脑风暴区,一篇文章容不下。
具备渲染架构、WebGL 封装根底后,接下来就该看最经典的模型(glTF)、3DTiles 的渲染架构设计了,旧版本的模型架构正在被新架构替换中,所以之后间接以新架构为根底解说。