乐趣区

关于cesium:CesiumJS-2022^-原理5-着色器相关的封装设计

本篇波及到的所有接口在公开文档中均无,须要下载 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 参数传递给 BufferVertexArray,因为 context 对象(类型 Context)是 WebGL 渲染上下文对象等底层接口的的封装对象,没有它无奈创立 WebGLBuffer 等原始接口对象。

所以,BufferVertexArray 并不是孤立的 API,必须与其它封装一起搭配来用,它们两个至多要依赖 Context 对象才行,在 1.4 中会介绍如何应用 Context 类封装 WebGL 底层接口并如何拜访 Context 对象的。

很少有须要间接创立 BufferVertexArray 的时候,应用这两个接口,就意味着你取得的数据合乎 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,还封装了数据上载的性能,只需安心地把贴图数据传入即可。

BufferVertexArrayTexture 也要 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 是 WebGLShaderWebGLProgram,顶点着色器和片元着色器独特形成一个着色器程序对象。在一帧的渲染中,由多个通道形成,每个通道在触发 draw 动作之前,通常要切换着色器程序,以达到不同的计算成果。

CesiumJS 的渲染远远简单于通用 Web3D,意味着有大量着色器程序。对象多了,就要治理。CesiumJS 封装了无关底层 API 的同时,还设计了缓存机制。

CesiumJS 应用 ShaderSource 类来治理着色器代码文本,应用 ShaderProgram 类来治理 WebGLProgramWebGLShader,应用 ShaderCache 类来缓存 ShaderProgram,再应用 ShaderFunctionShaderStructShaderDestination 来辅助 ShaderSource 解决着色器代码文本中的 glsl 函数、构造体成员、宏定义。

此外,还有一个 ShaderBuilder 类来辅助 ShaderProgram 的创立。

这一堆公有类与后面 BufferVertexArrayTexture 一样,并不能独自应用,通常是与第 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 类中。最外围的就是渲染上下文(WebGLRenderingContextWebGL2RenderingContext)对象了,除此之外,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 的外围执行局部,其实它也是用 DrawCommandClearCommand,在独立的 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
  })
})

这个 MaterialAppearanceAppearance 类的一个子类,除了上述这两个属性之外,还能够本人传递顶点着色器代码。

然而,通常不会间接向 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 对象给 Materialfabric 成员变量即可,这个对象能够自定义一种材质,能够具备 uniformMap,并在 glsl 代码中应用一个函数,返回一个 czm_material 构造体作为材质。

尽管能够创立 glsl 构造体作为材质,然而它仅仅只能作用于片元的局部着色过程。

Appearance API 作用的是 Primitive API 生成的图元对象,外观对象反对间接传递 Primitive 所需的两大着色器代码,然而也是有限度的,一些 vertex attritbutevarying 是必须存在的,而且还得本人解决渲染管线的转换,这方面材料较少。

通过下列代码,你能够输入最简略的 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]

PostProcessStageselected 是一个 js 数组,反对设定 Cesium3DTileFeatureLabelBillboard 等具备 pickId 拜访器,或 ModelCesium3DTilePointFeature 等具备 pickIds 拜访器的类为“选中对象”,并在更新过程(PostProcessStage.prototype.update)中创立一张抉择纹理。

无关后处理的材料当前可能会专门写利用篇来介绍。

3.3. 新架构带来的 CustomShader API

这个随同着 ModelExperimental 这个新架构(2022 年 5 月,此架构正在启动对原 Model 类相干架构的替换)的更新而随时可能会更新。

目前,CustomShader API 仅反对在 Cesium3DTilesetModelExperimental 两个类上应用。传入的自定义着色器作用在每个瓦片或模型上。

举例:

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 封装好的两个构造体 VertexInputFragmentInput,它们为你提供了尽可能详尽的值,譬如:

  • 顶点属性(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 的渲染架构设计了,旧版本的模型架构正在被新架构替换中,所以之后间接以新架构为根底解说。

退出移动版