cesium体渲染概述(一)

382 阅读8分钟

第一篇章:Cesium体渲染概述

引言

随着空间数据和三维地理信息的日益复杂,3D可视化和空间分析变得越来越重要。Cesium作为一个开源的Web端三维地球可视化引擎,提供了强大的工具集,使得用户能够在浏览器中加载、渲染和交互操作各种类型的空间数据。而在这些数据中,体积数据(Voxel Data)作为一种常见的三维数据形式,成为了很多应用场景中的关键组成部分。

体渲染(Voxel Rendering)是一种渲染体积数据的技术,它与传统的网格渲染不同,通过使用体素(Voxel)来表示和渲染三维空间中的数据。体素类似于像素,但它是一个三维的单元格,用于表示空间中的一小块区域。通过体渲染,Cesium能够有效地可视化大规模的体积数据,例如地形、气象数据、三维地图等。

本篇章将介绍Cesium体渲染的基本原理,重点讲解体渲染管线的各个阶段,包括数据加载、着色器更新、裁剪与视口判断、渲染命令的生成等,同时也会展示一些具体的代码示例,帮助开发者理解如何在Cesium中实现高效的体渲染。

TilesBuilderTilesBuilder提供一个高效、兼容、优化的数据转换工具,一站式完成数据转换、数据发布、数据预览操作。 请添加图片描述

体渲染基础

体渲染(Voxel Rendering)是渲染三维体积数据的一种技术。与传统的网格渲染(Mesh Rendering)相比,体渲染使用体素(Voxel)来表示三维空间中的数据。体素通常是一个立方体或类似的单元,它能够表示一个三维空间元素。每个体素包含一定的属性值,如颜色、密度、强度等,这些属性在渲染过程中被用来计算物体的外观。

在Cesium中,体渲染技术被广泛应用于地形数据的可视化、体积数据的展示以及其他空间分析任务。例如,地形数据的表示通常通过体积数据来实现,这使得Cesium能够呈现更为丰富和准确的三维地球视图。

请添加图片描述

Cesium体渲染管线

Cesium的体渲染过程包括多个关键阶段,涉及数据初始化、着色器更新、体素数据处理、裁剪与渲染命令生成等。下面将详细介绍这些阶段,并结合具体代码示例进行解释。

1. 数据准备与初始化

在Cesium中,体渲染通常需要从一个数据提供者(provider)获取数据,并根据数据的状态进行初始化。数据提供者可以是一个体素数据集、一个栅格图像,或者其他空间数据源。在初次渲染时,Cesium会进行初始化操作,例如加载体素数据、设置变换矩阵等。

VoxelPrimitive.prototype.update = function (frameState) {
  const provider = this._provider;
  const context = frameState.context;

  // 仅在第一次调用时执行初始化
  if (!this._ready) {
    initFromProvider(this, provider, context);
    
    // 在第一次渲染后标记为已准备好
    frameState.afterRender.push(() => {
      this._ready = true;
      return true;
    });

    // 返回,直到下一帧再开始渲染
    return;
  }
};

这段代码展示了如何在第一次渲染时初始化数据,并在初始化完成后标记体素数据为“已准备好”,确保后续的渲染流程能够顺利进行。

2. 着色器更新与处理

体渲染的一个关键技术是自定义着色器。每一帧渲染时,Cesium会检查体素数据的变化,如果数据发生变化,需要重新编译和更新着色器。具体代码如下:

if (this._shaderDirty) {
  buildVoxelDrawCommands(this, context);
  this._shaderDirty = false;
}

当体素数据发生变化时,_shaderDirty标志会被设置为true,表示需要重新构建渲染命令并更新着色器。通过这一过程,Cesium能够保证每一帧渲染的数据和着色效果都能与最新的状态一致。

3. 体素数据更新

在体渲染中,体素数据可能会发生变化,因此每一帧都需要检查数据是否需要更新。以下代码检查体素数据是否已更改,并进行更新操作:

const shapeDirty = checkTransformAndBounds(this, provider);
const shape = this._shape;

if (shapeDirty) {
  this._shapeVisible = updateShapeAndTransforms(this, shape, provider);
  if (checkShapeDefines(this, shape)) {
    this._shaderDirty = true; // 标记着色器为脏
  }
}

这段代码在每一帧中检查体素形状和变换是否发生变化。如果数据发生了变化,标记着色器为脏(_shaderDirty = true),表示需要重新构建渲染命令。

4. 裁剪与视口判断

为了提升渲染性能,Cesium会在渲染前进行裁剪操作,剔除视口外的体素数据。具体代码如下:

const ndcAabb = orientedBoundingBoxToNdcAabb(
  orientedBoundingBox,
  transformPositionWorldToProjection,
  scratchNdcAabb,
);

const offscreen =
  ndcAabb.x === +1.0 ||
  ndcAabb.y === +1.0 ||
  ndcAabb.z === -1.0 ||
  ndcAabb.w === -1.0;

if (offscreen) {
  return; // 如果体素超出视口,跳过渲染
}

这段代码通过计算体素的AABB(轴对齐包围盒)在NDC空间中的位置,判断体素是否在视口内。如果超出视口范围,则跳过渲染,避免不必要的计算。

5. 渲染命令与提交

一旦所有更新操作完成,Cesium会生成渲染命令,并将其提交给渲染管线进行渲染。渲染命令包含着色器、纹理、变换矩阵等信息,用于控制如何绘制体素数据。

const command = frameState.passes.pick
  ? this._drawCommandPick
  : frameState.passes.pickVoxel
    ? this._drawCommandPickVoxel
    : this._drawCommand;

command.boundingVolume = shape.boundingSphere;
frameState.commandList.push(command);

根据当前渲染模式,选择合适的绘制命令,并将其添加到commandList中,等待渲染执行。

6. 调试与优化

Cesium还提供了调试功能,帮助开发者在渲染过程中监控和优化渲染效果。例如,使用debugDraw函数绘制体素数据的边界框:

if (this._debugDraw) {
  debugDraw(this, frameState); // 绘制调试信息
}

调试功能允许开发者实时查看渲染过程中的信息,帮助检测问题并优化性能。

7.完整代码
VoxelPrimitive.prototype.update = function (frameState) {
  const provider = this._provider;

  // 更新自定义着色器,处理可能的纹理uniforms
  this._customShader.update(frameState);

  // 从已经准备好的provider初始化,仅在第一次调用时执行
  const context = frameState.context;
  if (!this._ready) {
    initFromProvider(this, provider, context);
    
    // 在第一次渲染后标记为已准备好,确保订阅的事件能在第一次渲染后处理
    frameState.afterRender.push(() => {
      this._ready = true;
      return true;
    });

    // 返回,直到下一帧再开始渲染
    return;
  }

  // 更新竖直夸张(通常用于增强地形或体积的视觉效果)
  updateVerticalExaggeration(this, frameState);

  // 检查形状是否发生变化,通常在每一帧都需要重新计算
  const shapeDirty = checkTransformAndBounds(this, provider);
  const shape = this._shape;
  if (shapeDirty) {
    this._shapeVisible = updateShapeAndTransforms(this, shape, provider);
    if (checkShapeDefines(this, shape)) {
      this._shaderDirty = true; // 如果形状定义发生变化,标记着色器为脏
    }
  }
  if (!this._shapeVisible) {
    return; // 如果形状不可见,跳过渲染
  }

  // 更新遍历并准备渲染
  const keyframeLocation = getKeyframeLocation(
    provider.timeIntervalCollection,
    this._clock,
  );

  const traversal = this._traversal;
  const sampleCountOld = traversal._sampleCount;

  traversal.update(
    frameState,
    keyframeLocation,
    shapeDirty, // 是否需要重新计算边界
    this._disableUpdate, // 是否暂停更新
  );

  // 如果采样数量发生变化,标记着色器为脏
  if (sampleCountOld !== traversal._sampleCount) {
    this._shaderDirty = true;
  }

  if (!traversal.isRenderable(traversal.rootNode)) {
    return; // 如果遍历不可渲染,跳过渲染
  }

  // 如果启用了调试绘制,绘制调试信息(例如边界框)
  if (this._debugDraw) {
    debugDraw(this, frameState);
  }

  if (this._disableRender) {
    return; // 如果禁用了渲染,跳过渲染
  }

  // 检查是否使用了对数深度
  if (this._useLogDepth !== frameState.useLogDepth) {
    this._useLogDepth = frameState.useLogDepth;
    this._shaderDirty = true;
  }

  // 检查裁剪平面是否发生变化
  const clippingPlanesChanged = updateClippingPlanes(this, frameState);
  if (clippingPlanesChanged) {
    this._shaderDirty = true;
  }

  // 更新叶节点纹理的uniform
  const leafNodeTexture = traversal.leafNodeTexture;
  const uniforms = this._uniforms;
  if (defined(leafNodeTexture)) {
    uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture;
    uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone(
      traversal.leafNodeTexelSizeUv,
      uniforms.octreeLeafNodeTexelSizeUv,
    );
    uniforms.octreeLeafNodeTilesPerRow = traversal.leafNodeTilesPerRow;
  }

  // 如果着色器标记为脏,重新构建绘制命令
  if (this._shaderDirty) {
    buildVoxelDrawCommands(this, context);
    this._shaderDirty = false;
  }

  // 计算NDC空间的AABB,以便进行裁剪("scissor"操作)
  const transformPositionWorldToProjection =
    context.uniformState.viewProjection;
  const orientedBoundingBox = shape.orientedBoundingBox;
  const ndcAabb = orientedBoundingBoxToNdcAabb(
    orientedBoundingBox,
    transformPositionWorldToProjection,
    scratchNdcAabb,
  );

  // 如果对象超出视口范围,则不渲染
  const offscreen =
    ndcAabb.x === +1.0 ||
    ndcAabb.y === +1.0 ||
    ndcAabb.z === -1.0 ||
    ndcAabb.w === -1.0;
  if (offscreen) {
    return;
  }

  // 准备渲染:更新可能每帧变化的uniform
  uniforms.ndcSpaceAxisAlignedBoundingBox = Cartesian4.clone(
    ndcAabb,
    uniforms.ndcSpaceAxisAlignedBoundingBox,
  );

  // 更新视图变换矩阵
  const transformPositionViewToWorld = context.uniformState.inverseView;
  uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation(
    this._transformPositionWorldToUv,
    transformPositionViewToWorld,
    uniforms.transformPositionViewToUv,
  );
  const transformPositionWorldToView = context.uniformState.view;
  uniforms.transformPositionUvToView = Matrix4.multiplyTransformation(
    transformPositionWorldToView,
    this._transformPositionUvToWorld,
    uniforms.transformPositionUvToView,
  );

  // 更新方向变换矩阵
  const transformDirectionViewToWorld =
    context.uniformState.inverseViewRotation;
  uniforms.transformDirectionViewToLocal = Matrix3.multiply(
    this._transformDirectionWorldToLocal,
    transformDirectionViewToWorld,
    uniforms.transformDirectionViewToLocal,
  );
  uniforms.transformNormalLocalToWorld = Matrix3.clone(
    this._transformNormalLocalToWorld,
    uniforms.transformNormalLocalToWorld,
  );

  // 更新摄像机位置
  const cameraPositionWorld = frameState.camera.positionWC;
  uniforms.cameraPositionUv = Matrix4.multiplyByPoint(
    this._transformPositionWorldToUv,
    cameraPositionWorld,
    uniforms.cameraPositionUv,
  );

  // 设置步长(用于体素渲染)
  uniforms.stepSize = this._stepSizeMultiplier;

  // 根据当前渲染模式选择绘制命令
  const command = frameState.passes.pick
    ? this._drawCommandPick
    : frameState.passes.pickVoxel
      ? this._drawCommandPickVoxel
      : this._drawCommand;

  // 设置命令的包围球体(用于剔除)
  command.boundingVolume = shape.boundingSphere;

  // 将渲染命令推入命令列表
  frameState.commandList.push(command);
};

主要步骤总结:

  1. 初始化与着色器更新:检查并初始化来自 provider 的数据,并更新自定义着色器。
  2. 形状更新与检查:检查和更新体素形状、变换及边界框。
  3. 遍历更新:更新遍历信息,判断是否需要重新渲染。
  4. 调试绘制与裁剪:在调试模式下绘制边界框,并根据 NDC 空间的 AABB 判断对象是否超出视口。
  5. 计算与更新渲染矩阵:更新视图和投影矩阵、方向矩阵、摄像机矩阵等。
  6. 选择并推送渲染命令:根据渲染模式选择合适的绘制命令并推入渲染命令队列。

体渲染的关键技术

体渲染的实现依赖于多项技术,包括自定义着色器、数据裁剪、变换矩阵的更新等。具体来说,以下技术在体渲染中尤为重要:

  1. 自定义着色器:体素渲染依赖于自定义的着色器来处理三维体积数据的可视化。
  2. 裁剪与剔除:通过裁剪操作避免渲染视口外的体素数据,提高渲染性能。
  3. 变换矩阵与摄像机:不断更新视图和投影矩阵,确保体素数据正确地映射到屏幕上。
  4. 多级细节与分层渲染:根据物体与摄像机的距离动态选择不同的细节级别,减少远距离体素的渲染计算。

总结

Cesium中的体渲染是一个复杂但高度优化的过程,涉及从数据加载到渲染命令生成的各个环节。通过自定义着色器、精确的裁剪和高效的数据更新策略,Cesium能够渲染出高质量的体积数据,广泛应用于地形可视化、气象模拟等领域。

在后续的篇章中,后面再说。

TilesBuilderTilesBuilder提供一个高效、兼容、优化的数据转换工具,一站式完成数据转换、数据发布、数据预览操作。

请添加图片描述